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

feat: Graphql verbatim search terms #1174

Merged
2 changes: 1 addition & 1 deletion packages/common/src/enums/searchTerm.type.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type SearchTerm = string | number | boolean | Date;
export type SearchTerm = string | number | boolean | Date | null;
2 changes: 1 addition & 1 deletion packages/common/src/filters/sliderFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class SliderFilter implements Filter {
sliderVals = (term1 as string).split('..');
this._currentValue = +(sliderVals?.[0] ?? 0);
} else if (hasData(term1) || term1 === '') {
this._currentValue = +term1;
this._currentValue = term1 === null ? undefined : +term1;
sliderVals = [term1 as string | number];
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/interfaces/currentFilter.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ export interface CurrentFilter {

/** Target element selector from which the filter was triggered from. */
targetSelector?: string;

/**
* When false, searchTerms may be manipulated to be functional with certain filters eg: string only filters.
* When true, JSON.stringify is used on the searchTerms and used in the query "as-is". It is then the responsibility of the developer to sanitise the `searchTerms` property if necessary.
*/
verbatimSearchTerms?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface GraphqlServiceOption extends BackendServiceOption {
*/
extraQueryArguments?: QueryArgument[];

/** (NOT FULLY IMPLEMENTED) Is the GraphQL Server using cursors? */
/** Is the GraphQL Server using cursors? */
isWithCursor?: boolean;

/** What are the pagination options? ex.: (first, last, offset) */
Expand All @@ -40,4 +40,10 @@ export interface GraphqlServiceOption extends BackendServiceOption {
* ex.: { field: "name", operator: EQ, value: "John" }
*/
keepArgumentFieldDoubleQuotes?: boolean;

/**
* When false, searchTerms may be manipulated to be functional with certain filters eg: string only filters.
* When true, JSON.stringify is used on the searchTerms and used in the query "as-is". It is then the responsibility of the developer to sanitise the `searchTerms` property if necessary.
*/
useVerbatimSearchTerms?: boolean;
ghiscoding marked this conversation as resolved.
Show resolved Hide resolved
}
38 changes: 38 additions & 0 deletions packages/graphql/src/services/__tests__/graphql.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,44 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
expect(currentFilters).toEqual([]);
});

describe("Verbatim ColumnFilters", () => {
describe.each`
description | verbatim | operator | searchTerms | expectation
${"Verbatim false, Filter for null"} | ${false} | ${'EQ'} | ${null} | ${'query{users(first:10, offset:0) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Filter for null"} | ${true} | ${'EQ'} | ${null} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:EQ, value:"null"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim false, Empty string"} | ${false} | ${'EQ'} | ${''} | ${'query{users(first:10, offset:0) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Empty string"} | ${true} | ${'EQ'} | ${''} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:EQ, value:"\\"\\""}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim false, Empty list"} | ${false} | ${'IN'} | ${[]} | ${'query{users(first:10, offset:0) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Empty list"} | ${true} | ${'IN'} | ${[]} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"[]"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim false, Filter for null (in list)"} | ${false} | ${'IN'} | ${[null]} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:""}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Filter for null (in list)"} | ${true} | ${'IN'} | ${[null]} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"[null]"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim false, Filter for empty string (in list)"} | ${false} | ${'IN'} | ${['']} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:""}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Filter for empty string (in list)"} | ${true} | ${'IN'} | ${['']} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"[\\"\\"]"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim false, Filter for female"} | ${false} | ${'IN'} | ${['female']} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"female"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Filter for female"} | ${true} | ${'IN'} | ${['female']} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"[\\"female\\"]"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim false, Filter for female/male"} | ${false} | ${'IN'} | ${['female', 'male']} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"female, male"}]) { totalCount,nodes{ id,company,gender,name } }}'}
${"Verbatim true, Filter for female/male"} | ${true} | ${'IN'} | ${['female', 'male']} | ${'query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"[\\"female\\", \\"male\\"]"}]) { totalCount,nodes{ id,company,gender,name } }}'}
`(`$description`, ({ description, verbatim, operator, searchTerms, expectation }) => {

const mockColumn = { id: 'gender', field: 'gender' } as Column;
let mockColumnFilters: ColumnFilters;

beforeEach(() => {
mockColumnFilters = {
gender: { columnId: 'gender', columnDef: mockColumn, searchTerms, operator, type: FieldType.string, verbatimSearchTerms: verbatim },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
});

test(`buildQuery output matches ${expectation}`, () => {
const query = service.buildQuery();
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});
});
});
});

describe('presets', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/graphql/src/services/graphql.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,11 @@ export class GraphqlService implements BackendService {
throw new Error(`GraphQL filter could not find the field name to query the search, your column definition must include a valid "field" or "name" (optionally you can also use the "queryfield").`);
}

if (this.options?.useVerbatimSearchTerms || columnFilter.verbatimSearchTerms) {
searchByArray.push({ field: fieldName, operator: columnFilter.operator, value: JSON.stringify(columnFilter.searchTerms) });
continue;
}

fieldSearchValue = (fieldSearchValue === undefined || fieldSearchValue === null) ? '' : `${fieldSearchValue}`; // make sure it's a string

// run regex to find possible filter operators unless the user disabled the feature
Expand Down
6 changes: 6 additions & 0 deletions packages/odata/src/interfaces/odataOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export interface OdataOption extends BackendServiceOption {
/** OData (or any other) version number (the query string is different between versions) */
version?: number;

/**
* When false, searchTerms may be manipulated to be functional with certain filters eg: string only filters.
* When true, JSON.stringify is used on the searchTerms and used in the query "as-is". It is then the responsibility of the developer to sanitise the `searchTerms` property if necessary.
*/
useVerbatimSearchTerms?: boolean;

/** A callback which will extract and return the count from the data queried. Defaults to 'd.__count' for v2, '__count' for v3 and '@odata.count' for v4. */
countExtractor?: (response: any) => number;

Expand Down
40 changes: 40 additions & 0 deletions packages/odata/src/services/__tests__/grid-odata.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,46 @@ describe('GridOdataService', () => {
});
});

describe('updateFilters method', () => {
describe("Verbatim ColumnFilters", () => {
describe.each`
description | verbatim | operator | searchTerms | expectation
${"Verbatim false, Filter for null"} | ${false} | ${'EQ'} | ${null} | ${'$top=10'}
${"Verbatim true, Filter for null"} | ${true} | ${'EQ'} | ${null} | ${'$top=10&$filter=(gender EQ null)'}
${"Verbatim false, Empty string"} | ${false} | ${'EQ'} | ${''} | ${'$top=10'}
${"Verbatim true, Empty string"} | ${true} | ${'EQ'} | ${''} | ${'$top=10&$filter=(gender EQ \"\")'}
${"Verbatim false, Empty list"} | ${false} | ${'IN'} | ${[]} | ${'$top=10'}
${"Verbatim true, Empty list"} | ${true} | ${'IN'} | ${[]} | ${'$top=10&$filter=(gender IN [])'}
${"Verbatim false, Filter for null (in list)"} | ${false} | ${'IN'} | ${[null]} | ${'$top=10'}
${"Verbatim true, Filter for null (in list)"} | ${true} | ${'IN'} | ${[null]} | ${'$top=10&$filter=(gender IN [null])'}
${"Verbatim false, Filter for empty string (in list)"} | ${false} | ${'IN'} | ${['']} | ${'$top=10'}
${"Verbatim true, Filter for empty string (in list)"} | ${true} | ${'IN'} | ${['']} | ${'$top=10&$filter=(gender IN [\"\"])'}
${"Verbatim false, Filter for female"} | ${false} | ${'IN'} | ${['female']} | ${'$top=10&$filter=(Gender eq \'female\')'}
${"Verbatim true, Filter for female"} | ${true} | ${'IN'} | ${['female']} | ${'$top=10&$filter=(gender IN [\"female\"])'}
${"Verbatim false, Filter for female/male"} | ${false} | ${'IN'} | ${['female', 'male']} | ${'$top=10&$filter=(Gender eq \'female\' or Gender eq \'male\')'}
${"Verbatim true, Filter for female/male"} | ${true} | ${'IN'} | ${['female', 'male']} | ${'$top=10&$filter=(gender IN [\"female\",\"male\"])'}
`(`$description`, ({ description, verbatim, operator, searchTerms, expectation }) => {

const mockColumn = { id: 'gender', field: 'gender' } as Column;
let mockColumnFilters: ColumnFilters;

beforeEach(() => {
mockColumnFilters = {
gender: { columnId: 'gender', columnDef: mockColumn, searchTerms, operator, type: FieldType.string, verbatimSearchTerms: verbatim },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
});

test(`buildQuery output matches ${expectation}`, () => {
const query = service.buildQuery();
expect(query).toBe(expectation);
});
});
});
});

describe('presets', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
5 changes: 5 additions & 0 deletions packages/odata/src/services/grid-odata.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ export class GridOdataService implements BackendService {
throw new Error(`GridOData filter could not find the field name to query the search, your column definition must include a valid "field" or "name" (optionally you can also use the "queryfield").`);
}

if (this._odataService.options.useVerbatimSearchTerms || columnFilter.verbatimSearchTerms) {
searchByArray.push(`${fieldName} ${columnFilter.operator} ${JSON.stringify(columnFilter.searchTerms)}`.trim());
continue;
}

fieldSearchValue = (fieldSearchValue === undefined || fieldSearchValue === null) ? '' : `${fieldSearchValue}`; // make sure it's a string

// run regex to find possible filter operators unless the user disabled the feature
Expand Down