Skip to content

Commit

Permalink
feat(backend): add OData & GraphQL Service API interfaces
Browse files Browse the repository at this point in the history
- this will help user making sure they use valid options in their Backend Services
  • Loading branch information
ghiscoding-SE committed Jan 13, 2020
1 parent b894ee1 commit 11cc71c
Show file tree
Hide file tree
Showing 13 changed files with 93 additions and 62 deletions.
35 changes: 13 additions & 22 deletions src/app/examples/grid-graphql.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
GraphqlResult,
GraphqlPaginatedResult,
GraphqlService,
GraphqlServiceOption,
GraphqlServiceApi,
GridOption,
GridStateChange,
Metrics,
Expand Down Expand Up @@ -176,7 +176,17 @@ export class GridGraphqlComponent implements OnInit, OnDestroy {
},
backendServiceApi: {
service: new GraphqlService(),
options: this.getBackendOptions(this.isWithCursor),
options: {
datasetName: GRAPHQL_QUERY_DATASET_NAME, // the only REQUIRED property
addLocaleIntoQuery: true, // optionally add current locale into the query
extraQueryArguments: [{ // optionally add some extra query arguments as input query arguments
field: 'userId',
value: 123
}],
// 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}]) {}
keepArgumentFieldDoubleQuotes: true
},
// you can define the onInit callback OR enable the "executeProcessCommandOnInit" flag in the service init
// onInit: (query) => this.getCustomerApiCall(query)
preProcess: () => this.displaySpinner(true),
Expand All @@ -185,7 +195,7 @@ export class GridGraphqlComponent implements OnInit, OnDestroy {
this.metrics = result.metrics;
this.displaySpinner(false);
}
}
} as GraphqlServiceApi
};
}

Expand All @@ -201,25 +211,6 @@ export class GridGraphqlComponent implements OnInit, OnDestroy {
: { text: 'done', class: 'alert alert-success' };
}

getBackendOptions(withCursor: boolean): GraphqlServiceOption {
// with cursor, paginationOptions can be: { first, last, after, before }
// without cursor, paginationOptions can be: { first, last, offset }
return {
columnDefinitions: this.columnDefinitions,
datasetName: GRAPHQL_QUERY_DATASET_NAME,
isWithCursor: withCursor,
addLocaleIntoQuery: true,
extraQueryArguments: [{
field: 'userId',
value: 123
}],

// 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}]) {}
keepArgumentFieldDoubleQuotes: true
};
}

/**
* Calling your GraphQL backend server should always return a Promise or Observable of type GraphqlPaginatedResult (or GraphqlResult)
*
Expand Down
4 changes: 3 additions & 1 deletion src/app/examples/grid-odata.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GridStateChange,
Metrics,
OdataOption,
OdataServiceApi,
OperatorType,
} from './../modules/angular-slickgrid';

Expand Down Expand Up @@ -109,6 +110,7 @@ export class GridOdataComponent implements OnInit {
backendServiceApi: {
service: new GridOdataService(),
options: {
// useLocalFiltering: true,
enableCount: this.isCountEnabled, // add the count in the OData query, which will return a property named "odata.count" (v2) or "@odata.count" (v4)
version: this.odataVersion // defaults to 2, the query string is slightly different between OData 2 and 4
} as OdataOption,
Expand All @@ -119,7 +121,7 @@ export class GridOdataComponent implements OnInit {
this.displaySpinner(false);
this.getCustomerCallback(response);
}
}
} as OdataServiceApi
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
import { Observable } from 'rxjs';

import { OdataOption } from './odataOption.interface';
import { BackendService } from './backendService.interface';
import { GraphqlResult } from './graphqlResult.interface';
import { GraphqlPaginatedResult } from './graphqlPaginatedResult.interface';
import { GraphqlServiceOption } from './graphqlServiceOption.interface';

export interface BackendServiceApi {
/** 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;

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

/** On init (or on page load), what action to perform? */
onInit?: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult | any> | Observable<GraphqlResult | GraphqlPaginatedResult | any>;
onInit?: (query: string) => Promise<any> | Observable<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/Observable. For example: this.http.get(myGraphqlUrl) */
process: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult | any> | Observable<GraphqlResult | GraphqlPaginatedResult | any>;
process: (query: string) => Promise<any> | Observable<any>;

/** After executing the query, what action to perform? For example, stop the spinner */
postProcess?: (response: GraphqlResult | GraphqlPaginatedResult | any) => void;
postProcess?: (response: 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;
Expand All @@ -35,5 +31,5 @@ export interface BackendServiceApi {
* 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;
internalPostProcess?: (result: any) => void;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { QueryArgument } from './queryArgument.interface';
import { BackendEventChanged } from './backendEventChanged.interface';
import { Column } from './column.interface';

export interface BackendServiceOption {
/** Array of column ids that are included in the column definitions */
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[];

/** What are the pagination options? ex.: (first, last, offset) */
paginationOptions?: any;
Expand All @@ -16,11 +18,4 @@ export interface BackendServiceOption {

/** 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[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Observable } from 'rxjs';

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> | Observable<GraphqlResult | GraphqlPaginatedResult>;

/** On Processing, we get the query back from the service, and we need to provide a Promise/Observable. For example: this.http.get(myGraphqlUrl) */
process: (query: string) => Promise<GraphqlResult | GraphqlPaginatedResult> | Observable<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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GraphqlFilteringOption } from './graphqlFilteringOption.interface';
import { GraphqlSortingOption } from './graphqlSortingOption.interface';
import { GraphqlCursorPaginationOption } from './graphqlCursorPaginationOption.interface';
import { GraphqlPaginationOption } from './graphqlPaginationOption.interface';
import { QueryArgument } from './queryArgument.interface';

export interface GraphqlServiceOption extends BackendServiceOption {
/**
Expand All @@ -13,10 +14,14 @@ export interface GraphqlServiceOption extends BackendServiceOption {
addLocaleIntoQuery?: boolean;

/** What is the dataset, this is required for the GraphQL query to be built */
datasetName?: string;
datasetName: string;

/** Column definitions, you can pass this instead of "columnIds" */
columnDefinitions?: Column[];
/**
* 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[];

/** (NOT FULLY IMPLEMENTED) Is the GraphQL Server using cursors? */
isWithCursor?: boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/app/modules/angular-slickgrid/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export * from './graphqlFilteringOption.interface';
export * from './graphqlPaginatedResult.interface';
export * from './graphqlPaginationOption.interface';
export * from './graphqlResult.interface';
export * from './graphqlServiceApi.interface';
export * from './graphqlServiceOption.interface';
export * from './graphqlSortingOption.interface';
export * from './gridMenu.interface';
Expand Down Expand Up @@ -103,6 +104,7 @@ export * from './metrics.interface';
export * from './multiColumnSort.interface';
export * from './multipleSelectOption.interface';
export * from './odataOption.interface';
export * from './odataServiceApi.interface';
export * from './odataSortingOption.interface';
export * from './onEventArgs.interface';
export * from './operatorString';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CaseType } from './caseType';

export interface OdataOption extends BackendServiceOption {
/** What is the casing type to use? Typically that would be 1 of the following 2: camelCase or PascalCase */
caseType?: CaseType;
caseType: CaseType;

/** Add the total count $inlinecount (OData v2) or $count (OData v4) to the OData query */
enableCount?: boolean;
Expand All @@ -20,7 +20,7 @@ export interface OdataOption extends BackendServiceOption {
/** Filter string (or array of string) that must be a valid OData string */
filterBy?: any;

/** What is separator between each filters? Typically "and", "or" */
/** What is the separator between each filters? Typically "and", "or" */
filterBySeparator?: 'and' | 'or';

/** Filter queue */
Expand All @@ -31,7 +31,4 @@ export interface OdataOption extends BackendServiceOption {

/** OData version number (the query string is different between versions) */
version?: number;

/** When accessed as an object */
[key: string]: any;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BackendServiceApi } from './backendServiceApi.interface';
import { OdataOption } from './odataOption.interface';
import { GridOdataService } from '../services';

export interface OdataServiceApi extends BackendServiceApi {
/** Backend Service Options */
options?: Partial<OdataOption>;

/** Backend Service instance (could be OData or GraphQL Service) */
service: GridOdataService;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ColumnSort,
CurrentFilter,
FilterChangedArgs,
GraphqlServiceApi,
GraphqlServiceOption,
GridOption,
MultiColumnSort,
Expand All @@ -27,10 +28,11 @@ const gridOptionMock = {
enablePagination: true,
backendServiceApi: {
service: undefined,
options: { datasetName: '' },
preProcess: jest.fn(),
process: jest.fn(),
postProcess: jest.fn(),
}
} as GraphqlServiceApi
} as GridOption;

const gridStub = {
Expand Down Expand Up @@ -105,7 +107,7 @@ describe('GraphqlService', () => {

it('should throw an error when no dataset is provided in the service options after service init', () => {
service.init({ datasetName: undefined });
expect(() => service.buildQuery()).toThrow();
expect(() => service.buildQuery()).toThrow('GraphQL Service requires the "datasetName" property to properly build the GraphQL query');
});

it('should throw an error when no column definitions is provided in the service options after service init', () => {
Expand Down
6 changes: 3 additions & 3 deletions src/app/modules/angular-slickgrid/services/graphql.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class GraphqlService implements BackendService {
/** Initialization of the service, which acts as a constructor */
init(serviceOptions?: GraphqlServiceOption, pagination?: Pagination, grid?: any): void {
this._grid = grid;
this.options = serviceOptions || {};
this.options = serviceOptions || { datasetName: '', columnDefinitions: [] };
this.pagination = pagination;

if (grid && grid.getColumns) {
Expand All @@ -71,7 +71,7 @@ export class GraphqlService implements BackendService {
*/
buildQuery() {
if (!this.options || !this.options.datasetName || (!this._columnDefinitions && !Array.isArray(this.options.columnDefinitions))) {
throw new Error('GraphQL Service requires "datasetName" & "columnDefinitions" properties for it to work');
throw new Error('GraphQL Service requires the "datasetName" property to properly build the GraphQL query');
}

// get the column definitions and exclude some if they were tagged as excluded
Expand Down Expand Up @@ -262,7 +262,7 @@ export class GraphqlService implements BackendService {
this.updateOptions({ paginationOptions });
}

updateOptions(serviceOptions?: GraphqlServiceOption) {
updateOptions(serviceOptions?: Partial<GraphqlServiceOption>) {
this.options = { ...this.options, ...serviceOptions };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class GridOdataService implements BackendService {
private _columnDefinitions: Column[];
private _grid: any;
private _odataService: OdataQueryBuilderService;
options: OdataOption;
options: Partial<OdataOption>;
pagination: Pagination | undefined;
defaultOptions: OdataOption = {
top: DEFAULT_ITEMS_PER_PAGE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class OdataQueryBuilderService {
_columnFilters: any;
_defaultSortBy: string;
_filterCount: number;
_odataOptions: OdataOption;
_odataOptions: Partial<OdataOption>;

constructor() {
this._odataOptions = {
Expand Down Expand Up @@ -86,11 +86,11 @@ export class OdataQueryBuilderService {
return this._columnFilters;
}

get options(): OdataOption {
get options(): Partial<OdataOption> {
return this._odataOptions;
}

set options(options: OdataOption) {
set options(options: Partial<OdataOption>) {
this._odataOptions = options;
}

Expand All @@ -111,7 +111,7 @@ export class OdataQueryBuilderService {
* Change any OData options that will be used to build the query
* @param object options
*/
updateOptions(options: OdataOption) {
updateOptions(options: Partial<OdataOption>) {
for (const property of Object.keys(options)) {
if (options.hasOwnProperty(property)) {
this._odataOptions[property] = options[property]; // replace of the property
Expand Down

0 comments on commit 11cc71c

Please sign in to comment.