Skip to content

Commit

Permalink
feat(GraphQL): add filterQueryOverride to GraphQL Service (#1549)
Browse files Browse the repository at this point in the history
* feat(GraphQL): add `filterOverride` to GraphQL Service
  • Loading branch information
ghiscoding authored May 29, 2024
1 parent 3882ce1 commit 2c0a493
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 73 deletions.
22 changes: 22 additions & 0 deletions docs/backend-services/graphql/GraphQL-Filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- [filterBy](#filterby)
- [Complex Objects](#complex-objects)
- [Extra Query Arguments](#extra-query-arguments)
- [Override the filter query](#override-the-filter-query)

### Introduction
The implementation of a GraphQL Service requires a certain structure to follow for `Slickgrid-Universal` to work correctly (it will fail if your GraphQL Schema is any different than what is shown below).
Expand Down Expand Up @@ -131,4 +132,25 @@ this.gridOptions = {
}
}
}
```

### Override the filter query

Column filters may have a `Custom` Operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the GraphQL Options. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled.

E.g. you could listen for a specific column and the active `OperatorType.custom` in order to switch the filter to an SQL LIKE search in GraphQL:

> **Note** technically speaking GraphQL isn't a database query language like SQL, it's an application query language. Depending on your configuration, your GraphQL Server might already support regex querying (e.g. Hasura [_regex](https://hasura.io/docs/latest/queries/postgres/filters/text-search-operators/#_regex)) or you could add your own implementation (e.g. see this SO: https://stackoverflow.com/a/37981802/1212166). Just make sure that whatever custom operator that you want to use, is already included in your GraphQL Schema.
```ts
backendServiceApi: {
options: {
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValue }) => {
if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
// the `operator` is a string, make sure to implement this new operator in your GraphQL Schema
return { field: fieldName, operator: 'Like', value: searchValue };
}
},
}
}
```
7 changes: 3 additions & 4 deletions docs/grid-functionalities/Export-to-Excel.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ export class MyExample {
// push empty data on A1
cols.push({ value: '' });
// push data in B1 cell with metadata formatter
cols.push({
value: customTitle,
metadata: { style: excelFormat.id }
cols.push({
value: customTitle,
metadata: { style: excelFormat.id }
});
sheet.data.push(cols);
}
Expand Down Expand Up @@ -315,7 +315,6 @@ Below is a preview of the previous customizations shown above
![image](https://user-images.githubusercontent.com/643976/208590003-b637dcda-5164-42cc-bfad-e921a22c1837.png)
### Cell Format Auto-Detect Disable
##### requires `v3.2.0` or higher
The system will auto-detect the Excel format to use for Date and Number field types, if for some reason you wish to disable it then you provide the excel export options below
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ <h3 class="title is-3">
</h3>
<h6 class="title is-6 italic">
<span class="text-red">(*) NO DATA SHOWN</span>
- just change any of Filters/Sorting/Pages and look at the "GraphQL Query" changing :)
- just change any of Filters/Sorting/Pages and look at the "GraphQL Query" changing.
Also note that the column Name has a filter with a custom %% operator that behaves like an SQL LIKE operator supporting % wildcards.
Depending on your configuration, your GraphQL Server might already support regex querying (e.g. Hasura [_regex](https://hasura.io/docs/latest/queries/postgres/filters/text-search-operators/#_regex))
or you could add your own implementation (e.g. see this SO: https://stackoverflow.com/a/37981802/1212166).
</h6>

<div class="row">
Expand Down
23 changes: 22 additions & 1 deletion examples/vite-demo-vanilla-bundle/src/examples/example10.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,15 @@ export default class Example10 {
sortable: true,
filterable: true,
filter: {
model: Filters.compoundInput
model: Filters.compoundInput,
compoundOperatorList: [
{ operator: '', desc: 'Contains' },
{ operator: '<>', desc: 'Not Contains' },
{ operator: '=', desc: 'Equals' },
{ operator: '!=', desc: 'Not equal to' },
{ operator: 'a*', desc: 'Starts With' },
{ operator: 'Custom', desc: 'SQL Like' },
],
}
},
{
Expand Down Expand Up @@ -144,6 +152,10 @@ export default class Example10 {
enableAutoResize: false,
gridHeight: 275,
gridWidth: 900,
compoundOperatorAltTexts: {
// where '=' is any of the `OperatorString` type shown above
text: { 'Custom': { operatorAlt: '%%', descAlt: 'SQL Like' } },
},
enableFiltering: true,
enableCellNavigation: true,
createPreHeaderPanel: true,
Expand Down Expand Up @@ -193,6 +205,15 @@ export default class Example10 {
field: 'userId',
value: 123
}],
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValue }) => {
if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
// technically speaking GraphQL isn't a database query language like SQL, it's an application query language.
// What that means is that GraphQL won't let you write arbitrary queries out of the box.
// It will only support the types of queries defined in your GraphQL schema.
// see this SO: https://stackoverflow.com/a/37981802/1212166
return { field: fieldName, operator: 'Like', value: searchValue };
}
},
useCursor: this.isWithCursor, // sets pagination strategy, if true requires a call to setPageInfo() when graphql call returns
// when dealing with complex objects, we want to keep our field name with double quotes
// example with gender: query { users (orderBy:[{field:"gender",direction:ASC}]) {}
Expand Down
5 changes: 3 additions & 2 deletions examples/vite-demo-vanilla-bundle/src/examples/example23.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class CustomSumAggregator implements Aggregator {
export default class Example19 {
private _bindingEventService: BindingEventService;
columnDefinitions: Column<GroceryItem>[] = [];
dataset: any[] = [];
dataset: GroceryItem[] = [];
gridOptions!: GridOption;
gridContainerElm: HTMLDivElement;
sgb: SlickVanillaGridBundle;
Expand Down Expand Up @@ -287,6 +287,7 @@ export default class Example19 {
maxDecimal: 2,
minDecimal: 2,
},
enableGrouping: true,
externalResources: [this.excelExportService],
enableExcelExport: true,
excelExportOptions: {
Expand Down Expand Up @@ -426,7 +427,7 @@ export default class Example19 {
{ id: i++, name: 'Chicken Wings', qty: 12, taxable: true, price: .53 },
{ id: i++, name: 'Drinkable Yogurt', qty: 6, taxable: true, price: 1.22 },
{ id: i++, name: 'Milk', qty: 3, taxable: true, price: 3.11 },
];
] as GroceryItem[];

return datasetTmp;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,14 @@ export interface GraphqlFilteringOption {
/** Value to use when filtering */
value: any | any[];
}

export interface GraphqlCustomFilteringOption {
/** Field name to use when filtering */
field: string;

/** Custom Operator to use when filtering. Please note that any new Custom Operator must be implemented in your GraphQL Schema. */
operator: OperatorType | OperatorString;

/** Value to use when filtering */
value: any | any[];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BackendServiceOption } from '@slickgrid-universal/common';
import type { BackendServiceOption, BackendServiceFilterQueryOverrideArgs } from '@slickgrid-universal/common';

import type { GraphqlFilteringOption } from './graphqlFilteringOption.interface';
import type { GraphqlCustomFilteringOption, GraphqlFilteringOption } from './graphqlFilteringOption.interface';
import type { GraphqlSortingOption } from './graphqlSortingOption.interface';
import type { GraphqlCursorPaginationOption } from './graphqlCursorPaginationOption.interface';
import type { GraphqlPaginationOption } from './graphqlPaginationOption.interface';
Expand Down Expand Up @@ -29,6 +29,9 @@ export interface GraphqlServiceOption extends BackendServiceOption {
/** array of Filtering Options, ex.: { field: name, operator: EQ, value: "John" } */
filteringOptions?: GraphqlFilteringOption[];

/** An optional predicate function to overide the built-in filter construction */
filterQueryOverride?: (args: BackendServiceFilterQueryOverrideArgs) => GraphqlCustomFilteringOption | undefined;

/** What are the pagination options? ex.: (first, last, offset) */
paginationOptions?: GraphqlPaginationOption | GraphqlCursorPaginationOption;

Expand Down
44 changes: 44 additions & 0 deletions packages/graphql/src/services/__tests__/graphql.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,50 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should bypass default behavior if filterQueryOverride is defined and does not return undefined', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:foo, operator:EQ, value:"bar"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'name', field: 'name' } as Column;
const mockColumnFilters = {
name: { columnId: 'name', columnDef: mockColumn, searchTerms: ['Ca*le'], operator: 'a*z', type: FieldType.string },
} as ColumnFilters;

const sOptions = { ...serviceOptions, filterQueryOverride: () => ({ field: 'foo', operator: OperatorType.equal, value: 'bar' }) };
service.init(sOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

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

it('should continue with default behavior if filterQueryOverride returns undefined', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:name, operator:StartsWith, value:"Ca"},{field:name, operator:EndsWith, value:"le"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'name', field: 'name' } as Column;
const mockColumnFilters = {
name: { columnId: 'name', columnDef: mockColumn, searchTerms: ['Ca*le'], operator: 'a*z', type: FieldType.string },
} as ColumnFilters;

const sOptions = { ...serviceOptions, filterQueryOverride: () => undefined };
service.init(sOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

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

it('should continue with default behavior if filterQueryOverride is not provided', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:name, operator:StartsWith, value:"Ca"},{field:name, operator:EndsWith, value:"le"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'name', field: 'name' } as Column;
const mockColumnFilters = {
name: { columnId: 'name', columnDef: mockColumn, searchTerms: ['Ca*le'], operator: 'a*z', type: FieldType.string },
} 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 the operator Greater of Equal when the search value was provided as ">=10"', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:age, operator:GE, value:"10"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'age', field: 'age' } as Column;
Expand Down
Loading

0 comments on commit 2c0a493

Please sign in to comment.