Skip to content

Commit

Permalink
Merge pull request #283 from ghiscoding/feat/backend-service-features
Browse files Browse the repository at this point in the history
feat(backend): add OData & GraphQL Service API interfaces
- this will help user making sure they use valid options in their Backend Services
- also add 2x new flags (useLocalFiltering & useLocalSorting) that could optionally be used with a backend service
  • Loading branch information
ghiscoding committed Jan 15, 2020
2 parents 90accc1 + c27f807 commit 04f4490
Show file tree
Hide file tree
Showing 31 changed files with 553 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
SharedService,
SortService,
} from '../../services';
import { GridOption, CurrentFilter, CurrentSorter, GridStateType, Pagination, GridState, Column } from '../../models';
import { Column, CurrentFilter, CurrentSorter, GraphqlServiceApi, GraphqlServiceOption, GridOption, GridState, GridStateType, Pagination } from '../../models';
import { Filters } from '../../filters';
import { Editors } from '../../editors';
import * as utilities from '../../services/backend-utilities';
Expand Down Expand Up @@ -599,6 +599,26 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =

expect(syncSpy).toHaveBeenCalledWith(customElement.grid, true, false);
});

it('should bind local filter when "enableFiltering" is set', () => {
const bindLocalSpy = jest.spyOn(filterServiceStub, 'bindLocalOnFilter');

customElement.gridOptions = { enableFiltering: true } as GridOption;
customElement.bind();
customElement.attached();

expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
});

it('should bind local sort when "enableSorting" is set', () => {
const bindLocalSpy = jest.spyOn(sortServiceStub, 'bindLocalOnSort');

customElement.gridOptions = { enableSorting: true } as GridOption;
customElement.bind();
customElement.attached();

expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
});
});

describe('flag checks', () => {
Expand Down Expand Up @@ -971,7 +991,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =

it('should execute backend service "init" method when set', () => {
const mockPagination = { pageNumber: 1, pageSizes: [10, 25, 50], pageSize: 10, totalItems: 100 };
const mockGraphqlOptions = { extraQueryArguments: [{ field: 'userId', value: 123 }] };
const mockGraphqlOptions = { datasetName: 'users', extraQueryArguments: [{ field: 'userId', value: 123 }] } as GraphqlServiceOption;
const bindBackendSpy = jest.spyOn(sortServiceStub, 'bindBackendOnSort');
const mockGraphqlService2 = { ...mockGraphqlService, init: jest.fn() } as unknown as GraphqlService;
const initSpy = jest.spyOn(mockGraphqlService2, 'init');
Expand All @@ -982,8 +1002,8 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
service: mockGraphqlService2,
options: mockGraphqlOptions,
preProcess: () => jest.fn(),
process: (query) => new Promise((resolve) => resolve('process resolved')),
},
process: (query) => new Promise((resolve) => resolve({ data: { users: { nodes: [], totalCount: 100 } } })),
} as GraphqlServiceApi,
pagination: mockPagination,
} as GridOption;
customElement.bind();
Expand All @@ -993,34 +1013,42 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
expect(initSpy).toHaveBeenCalledWith(mockGraphqlOptions, mockPagination, mockGrid);
});

it('should bind local sort when "enableSorting" is set', () => {
const bindLocalSpy = jest.spyOn(sortServiceStub, 'bindLocalOnSort');
it('should call bind backend sorting when "enableSorting" is set', () => {
const bindBackendSpy = jest.spyOn(sortServiceStub, 'bindBackendOnSort');

customElement.gridOptions = { enableSorting: true } as GridOption;
customElement.gridOptions = {
enableSorting: true,
backendServiceApi: {
service: mockGraphqlService,
preProcess: () => jest.fn(),
process: (query) => new Promise((resolve) => resolve('process resolved')),
}
} as GridOption;
customElement.bind();
customElement.attached();

expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
expect(bindBackendSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
});

it('should reflect column filters when "enableSorting" is set', () => {
const bindBackendSpy = jest.spyOn(sortServiceStub, 'bindBackendOnSort');
it('should call bind local sorting when "enableSorting" is set and "useLocalSorting" is set as well', () => {
const bindLocalSpy = jest.spyOn(sortServiceStub, 'bindLocalOnSort');

customElement.gridOptions = {
enableSorting: true,
backendServiceApi: {
service: mockGraphqlService,
useLocalSorting: true,
preProcess: () => jest.fn(),
process: (query) => new Promise((resolve) => resolve('process resolved')),
}
} as GridOption;
customElement.bind();
customElement.attached();

expect(bindBackendSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
});

it('should reflect column filters when "enableFiltering" is set', () => {
it('should call bind backend filtering when "enableFiltering" is set', () => {
const initSpy = jest.spyOn(filterServiceStub, 'init');
const bindLocalSpy = jest.spyOn(filterServiceStub, 'bindLocalOnFilter');
const populateSpy = jest.spyOn(filterServiceStub, 'populateColumnFilterSearchTermPresets');
Expand All @@ -1034,6 +1062,24 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
expect(populateSpy).not.toHaveBeenCalled();
});

it('should call bind local filtering when "enableFiltering" is set and "useLocalFiltering" is set as well', () => {
const bindLocalSpy = jest.spyOn(filterServiceStub, 'bindLocalOnFilter');

customElement.gridOptions = {
enableFiltering: true,
backendServiceApi: {
service: mockGraphqlService,
useLocalFiltering: true,
preProcess: () => jest.fn(),
process: (query) => new Promise((resolve) => resolve('process resolved')),
}
} as GridOption;
customElement.bind();
customElement.attached();

expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid, mockDataView);
});

it('should reflect column filters when "enableFiltering" is set', () => {
const initSpy = jest.spyOn(filterServiceStub, 'init');
const bindBackendSpy = jest.spyOn(filterServiceStub, 'bindBackendOnFilter');
Expand Down
26 changes: 20 additions & 6 deletions src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,12 @@ export class AureliaSlickgridCustomElement {

// bind external sorting (backend) when available or default onSort (dataView)
if (gridOptions.enableSorting && !this.customDataView) {
gridOptions.backendServiceApi ? this.sortService.bindBackendOnSort(grid, dataView) : this.sortService.bindLocalOnSort(grid, dataView);
// bind external sorting (backend) unless specified to use the local one
if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalSorting) {
this.sortService.bindBackendOnSort(grid, dataView);
} else {
this.sortService.bindLocalOnSort(grid, dataView);
}
}

// bind external filter (backend) when available or default onFilter (dataView)
Expand All @@ -464,7 +469,12 @@ export class AureliaSlickgridCustomElement {
if (gridOptions.presets && Array.isArray(gridOptions.presets.filters) && gridOptions.presets.filters.length > 0) {
this.filterService.populateColumnFilterSearchTermPresets(gridOptions.presets.filters);
}
gridOptions.backendServiceApi ? this.filterService.bindBackendOnFilter(grid, this.dataview) : this.filterService.bindLocalOnFilter(grid, this.dataview);
// bind external filter (backend) unless specified to use the local one
if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalFiltering) {
this.filterService.bindBackendOnFilter(grid, dataView);
} else {
this.filterService.bindLocalOnFilter(grid, dataView);
}
}

// if user set an onInit Backend, we'll run it right away (and if so, we also need to run preProcess, internalPostProcess & postProcess)
Expand Down Expand Up @@ -628,6 +638,10 @@ export class AureliaSlickgridCustomElement {
gridOptions.gridId = this.gridId;
gridOptions.gridContainerId = `slickGridContainer-${this.gridId}`;

// if we have a backendServiceApi and the enablePagination is undefined, we'll assume that we do want to see it, else get that defined value
// @deprecated TODO remove this check in the future, user should explicitely enable the Pagination since this feature is now optional (you can now call OData/GraphQL without Pagination which is a new feature)
gridOptions.enablePagination = ((gridOptions.backendServiceApi && gridOptions.enablePagination === undefined) ? true : gridOptions.enablePagination) || false;

// use jquery extend to deep merge & copy to avoid immutable properties being changed in GlobalGridOptions after a route change
const options = $.extend(true, {}, GlobalGridOptions, gridOptions);

Expand Down Expand Up @@ -679,11 +693,11 @@ export class AureliaSlickgridCustomElement {
this.grid.invalidate();
this.grid.render();
}
if (this.gridOptions && this.gridOptions.backendServiceApi && this.gridOptions.pagination) {
// do we want to show pagination?
// if we have a backendServiceApi and the enablePagination is undefined, we'll assume that we do want to see it, else get that defined value
this.showPagination = ((this.gridOptions.backendServiceApi && this.gridOptions.enablePagination === undefined) ? true : this.gridOptions.enablePagination) || false;

// display the Pagination component only after calling this refresh data first, we call it here so that if we preset pagination page number it will be shown correctly
this.showPagination = ((this.gridOptions && this.gridOptions.enablePagination && (this.gridOptions.backendServiceApi && this.gridOptions.enablePagination === undefined)) ? true : this.gridOptions.enablePagination) || false;

if (this.gridOptions && this.gridOptions.backendServiceApi && this.gridOptions.pagination) {
if (this.gridOptions.presets && this.gridOptions.presets.pagination && this.gridOptions.pagination && this.paginationOptions) {
this.paginationOptions.pageSize = this.gridOptions.presets.pagination.pageSize;
this.paginationOptions.pageNumber = this.gridOptions.presets.pagination.pageNumber;
Expand Down
7 changes: 3 additions & 4 deletions src/aurelia-slickgrid/filters/selectFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { disposeAllSubscriptions, getDescendantProperty, htmlEncode } from '../s

@inject(BindingEngine, CollectionService, Optional.of(I18N))
export class SelectFilter implements Filter {
private _isFilterFirstRender = true;
private _isMultipleSelect = true;
private _locales: Locale;
private _shouldTriggerQuery = true;
Expand Down Expand Up @@ -111,7 +110,6 @@ export class SelectFilter implements Filter {
throw new Error('[Aurelia-SlickGrid] A filter must always have an "init()" with valid arguments.');
}

this._isFilterFirstRender = isFilterFirstRender;
this.grid = args.grid;
this.callback = args.callback;
this.columnDef = args.columnDef;
Expand Down Expand Up @@ -335,7 +333,8 @@ export class SelectFilter implements Filter {
}

// user can optionally add a blank entry at the beginning of the collection
if (this.collectionOptions && this.collectionOptions.addBlankEntry && this._isFilterFirstRender) {
// make sure however that it wasn't added more than once
if (this.collectionOptions && this.collectionOptions.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.labelName] !== '') {
collection.unshift(this.createBlankEntry());
}

Expand Down Expand Up @@ -400,7 +399,7 @@ export class SelectFilter implements Filter {
suffixText = (this.enableTranslateLabel && isEnableTranslate && suffixText && typeof suffixText === 'string') ? this.i18n && this.i18n.tr && this.i18n.tr(suffixText || ' ') : suffixText;
optionLabel = (this.enableTranslateLabel && isEnableTranslate && optionLabel && typeof optionLabel === 'string') ? this.i18n && this.i18n.tr && this.i18n.tr(optionLabel || ' ') : optionLabel;
// add to a temp array for joining purpose and filter out empty text
const tmpOptionArray = [prefixText, labelText !== undefined ? labelText.toString() : labelText, suffixText].filter((text) => text);
const tmpOptionArray = [prefixText, (typeof labelText === 'string' || typeof labelText === 'number') ? labelText.toString() : labelText, suffixText].filter((text) => text);
let optionText = tmpOptionArray.join(separatorBetweenLabels);

// if user specifically wants to render html text, he needs to opt-in else it will stripped out by default
Expand Down
2 changes: 1 addition & 1 deletion src/aurelia-slickgrid/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const Formatters = {
* Takes an array of complex objects converts it to a comma delimited string.
* Requires to pass an array of "propertyNames" in the column definition the generic "params" property
* For example, if we have an array of user objects that have the property of firstName & lastName then we need to pass in your column definition::
* { params: { propertyNames: ['firtName'] }}
* params: { propertyNames: ['firtName', 'lastName'] } => 'John Doe, Jane Doe'
*/
arrayObjectToCsv: arrayObjectToCsvFormatter,

Expand Down
41 changes: 29 additions & 12 deletions src/aurelia-slickgrid/models/backendServiceApi.interface.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
import { OdataOption } from './odataOption.interface';
import { GraphqlResult } from './graphqlResult.interface';
import { BackendService } from './backendService.interface';
import { GraphqlServiceOption } from './graphqlServiceOption.interface';
import { GraphqlPaginatedResult } from './graphqlPaginatedResult.interface';

export interface BackendServiceApi {
/** How long to wait until we start querying backend to avoid sending too many requests to backend server. Default to 750ms */
filterTypingDebounce?: number;

/** Backend Service Options */
options?: OdataOption | GraphqlServiceOption;
options?: any;

/** Backend Service instance (could be OData or GraphQL Service) */
/** Backend Service instance (could be OData, GraphQL or any other Backend Service) */
service: BackendService;

/**
* Do we want to optionally use the local (in memory) filtering strategy?
* This could be useful if user wishes to load the entire dataset only once with a OData/GraphQL Backend Service
* and then use local filter strategy (with SlickGrid DataView) with only current local dataset (only what got loaded in memory).
* This option could be used alone and/or with the "useLocalSorting" property.
*/
useLocalFiltering?: boolean;

/**
* Do we want to optionally use the local (in memory) sorting strategy?
* This could be useful if user wishes to load the entire dataset only once with a OData/GraphQL Backend Service
* and then use local sorting strategy (with SlickGrid DataView) with only current local dataset (only what got loaded in memory).
* This option could be used alone and/or with the "useLocalFiltering" property.
*/
useLocalSorting?: boolean;

// --
// available methods
// ------------------

/** On error callback, when an error is thrown by the process execution */
onError?: (e: any) => void;

/** On init (or on page load), what action to perform? */
onInit?: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult | any>;
onInit?: (query: string) => Promise<any>;

/** Before executing the query, what action to perform? For example, start a spinner */
preProcess?: () => void;

/** On Processing, we get the query back from the service, and we need to provide a Promise. For example: this.http.get(myGraphqlUrl) */
process: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult | any>;
process: (query: string) => Promise<any>;

/** After executing the query, what action to perform? For example, stop the spinner */
postProcess?: (response: GraphqlResult | GraphqlPaginatedResult | any) => void;

/** How long to wait until we start querying backend to avoid sending too many requests to backend server. Default to 750ms */
filterTypingDebounce?: number;
postProcess?: (response: any) => void;

/**
* INTERNAL USAGE ONLY by Aurelia-Slickgrid
* This internal process will be run just before postProcess and is meant to refresh the Dataset & Pagination after a GraphQL call
*/
internalPostProcess?: (result: GraphqlResult | GraphqlPaginatedResult) => void;
internalPostProcess?: (result: any) => void;
}
21 changes: 8 additions & 13 deletions src/aurelia-slickgrid/models/backendServiceOption.interface.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { BackendEventChanged } from './backendEventChanged.interface';
import { QueryArgument } from './queryArgument.interface';
import { Column } from './column.interface';

export interface BackendServiceOption {
/** What is the dataset name, this is required for the GraphQL query to be built */
datasetName?: string;
/**
* @deprecated (no longer required since the service is always initialized with the grid object and we can get the column definitions from there)
* Column definitions, used by the Backend Service to build the query according to the columns defined in the grid
*/
columnDefinitions?: Column[];

/** Pagination options (itemsPerPage, pageSize, pageSizes) */
/** What are the pagination options? ex.: (first, last, offset) */
paginationOptions?: any;

/** array of Filtering Options, ex.: [{ field: 'firstName', operator: 'EQ', value: 'John' }] */
filteringOptions?: any[];

/** array of Sorting Options, ex.: [{ field: 'firstName', direction: 'DESC' }] */
/** array of Filtering Options, ex.: [{ field: 'firstName', direction: 'DESC' }] */
sortingOptions?: any[];

/** Execute the process callback command on component init (page load) */
executeProcessCommandOnInit?: boolean;

/**
* Extra query arguments that be passed in addition to the default query arguments
* For example in GraphQL, if we want to pass "userId" and we want the query to look like
* users (first: 20, offset: 10, userId: 123) { ... }
*/
extraQueryArguments?: QueryArgument[];
}
28 changes: 28 additions & 0 deletions src/aurelia-slickgrid/models/graphqlServiceApi.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BackendServiceApi } from './backendServiceApi.interface';
import { GraphqlResult } from './graphqlResult.interface';
import { GraphqlPaginatedResult } from './graphqlPaginatedResult.interface';
import { GraphqlServiceOption } from './graphqlServiceOption.interface';
import { GraphqlService } from '../services';

export interface GraphqlServiceApi extends BackendServiceApi {
/** Backend Service Options */
options: GraphqlServiceOption;

/** Backend Service instance (could be OData or GraphQL Service) */
service: GraphqlService;

/** On init (or on page load), what action to perform? */
onInit?: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult>;

/** On Processing, we get the query back from the service, and we need to provide a Promise. For example: this.http.get(myGraphqlUrl) */
process: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult>;

/** After executing the query, what action to perform? For example, stop the spinner */
postProcess?: (response: GraphqlResult | GraphqlPaginatedResult) => void;

/**
* INTERNAL USAGE ONLY by Angular-Slickgrid
* This internal process will be run just before postProcess and is meant to refresh the Dataset & Pagination after a GraphQL call
*/
internalPostProcess?: (result: GraphqlResult | GraphqlPaginatedResult) => void;
}
Loading

0 comments on commit 04f4490

Please sign in to comment.