@slickgrid-universal/graphql 4.0.3 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,716 @@
1
+ import type {
2
+ BackendService,
3
+ Column,
4
+ ColumnFilter,
5
+ ColumnFilters,
6
+ ColumnSort,
7
+ CurrentFilter,
8
+ CurrentPagination,
9
+ CurrentSorter,
10
+ FilterChangedArgs,
11
+ GridOption,
12
+ MultiColumnSort,
13
+ OperatorString,
14
+ Pagination,
15
+ PaginationChangedArgs,
16
+ PaginationCursorChangedArgs,
17
+ SharedService,
18
+ SingleColumnSort,
19
+ SlickGrid,
20
+ SortDirectionString,
21
+ } from '@slickgrid-universal/common';
22
+ import {
23
+ FieldType,
24
+ // utilities
25
+ mapOperatorType,
26
+ mapOperatorByFieldType,
27
+ OperatorType,
28
+ SortDirection,
29
+ } from '@slickgrid-universal/common';
30
+ import { stripTags } from '@slickgrid-universal/utils';
31
+
32
+ import {
33
+ GraphqlCursorPaginationOption,
34
+ GraphqlDatasetFilter,
35
+ GraphqlFilteringOption,
36
+ GraphqlPaginationOption,
37
+ GraphqlServiceOption,
38
+ GraphqlSortingOption,
39
+ } from '../interfaces/index';
40
+
41
+ import QueryBuilder from './graphqlQueryBuilder';
42
+
43
+ const DEFAULT_ITEMS_PER_PAGE = 25;
44
+ const DEFAULT_PAGE_SIZE = 20;
45
+
46
+ export class GraphqlService implements BackendService {
47
+ protected _currentFilters: ColumnFilters | CurrentFilter[] = [];
48
+ protected _currentPagination: CurrentPagination | null = null;
49
+ protected _currentSorters: CurrentSorter[] = [];
50
+ protected _columnDefinitions?: Column[];
51
+ protected _grid: SlickGrid | undefined;
52
+ protected _datasetIdPropName = 'id';
53
+ options: GraphqlServiceOption | undefined;
54
+ pagination: Pagination | undefined;
55
+ defaultPaginationOptions: GraphqlPaginationOption = {
56
+ first: DEFAULT_ITEMS_PER_PAGE,
57
+ offset: 0
58
+ };
59
+
60
+ /** Getter for the Column Definitions */
61
+ get columnDefinitions() {
62
+ return this._columnDefinitions;
63
+ }
64
+
65
+ /** Getter for the Grid Options pulled through the Grid Object */
66
+ protected get _gridOptions(): GridOption {
67
+ return this._grid?.getOptions() ?? {} as GridOption;
68
+ }
69
+
70
+ /** Initialization of the service, which acts as a constructor */
71
+ init(serviceOptions?: GraphqlServiceOption, pagination?: Pagination, grid?: SlickGrid, sharedService?: SharedService): void {
72
+ this._grid = grid;
73
+ this.options = serviceOptions || { datasetName: '' };
74
+ this.pagination = pagination;
75
+ this._datasetIdPropName = this._gridOptions.datasetIdPropertyName || 'id';
76
+
77
+ if (grid?.getColumns) {
78
+ this._columnDefinitions = sharedService?.allColumns ?? grid.getColumns() ?? [];
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Build the GraphQL query, since the service include/exclude cursor, the output query will be different.
84
+ * @param serviceOptions GraphqlServiceOption
85
+ */
86
+ buildQuery() {
87
+ if (!this.options || !this.options.datasetName || !Array.isArray(this._columnDefinitions)) {
88
+ throw new Error('GraphQL Service requires the "datasetName" property to properly build the GraphQL query');
89
+ }
90
+
91
+ // get the column definitions and exclude some if they were tagged as excluded
92
+ let columnDefinitions = this._columnDefinitions || [];
93
+ columnDefinitions = columnDefinitions.filter((column: Column) => !column.excludeFromQuery);
94
+
95
+ const queryQb = new QueryBuilder(`query ${this.options.operationName ?? ''}`);
96
+ const datasetQb = new QueryBuilder(this.options.datasetName);
97
+ const nodesQb = new QueryBuilder('nodes');
98
+
99
+ // get all the columnds Ids for the filters to work
100
+ const columnIds: string[] = [];
101
+ if (columnDefinitions && Array.isArray(columnDefinitions)) {
102
+ for (const column of columnDefinitions) {
103
+ if (!column.excludeFieldFromQuery) {
104
+ columnIds.push(column.field);
105
+ }
106
+
107
+ // when extra "fields" are provided, also push them to columnIds
108
+ if (column.fields) {
109
+ columnIds.push(...column.fields);
110
+ }
111
+ }
112
+ }
113
+
114
+ // Slickgrid also requires the "id" field to be part of DataView
115
+ // add it to the GraphQL query if it wasn't already part of the list
116
+ if (columnIds.indexOf(this._datasetIdPropName) === -1) {
117
+ columnIds.unshift(this._datasetIdPropName);
118
+ }
119
+
120
+ const columnsQuery = this.buildFilterQuery(columnIds);
121
+ let graphqlNodeFields = [];
122
+
123
+ if (this._gridOptions.enablePagination !== false) {
124
+ if (this.options.useCursor) {
125
+ // ...pageInfo { hasNextPage, endCursor }, edges { cursor, node { _columns_ } }, totalCount: 100
126
+ const edgesQb = new QueryBuilder('edges');
127
+ const pageInfoQb = new QueryBuilder('pageInfo');
128
+ pageInfoQb.find('hasNextPage', 'hasPreviousPage', 'endCursor', 'startCursor');
129
+ nodesQb.find(columnsQuery);
130
+ edgesQb.find(['cursor']);
131
+ graphqlNodeFields = ['totalCount', nodesQb, pageInfoQb, edgesQb];
132
+ } else {
133
+ // ...nodes { _columns_ }, totalCount: 100
134
+ nodesQb.find(columnsQuery);
135
+ graphqlNodeFields = ['totalCount', nodesQb];
136
+ }
137
+ // all properties to be returned by the query
138
+ datasetQb.find(graphqlNodeFields);
139
+ } else {
140
+ // include all columns to be returned
141
+ datasetQb.find(columnsQuery);
142
+ }
143
+
144
+ // add dataset filters, could be Pagination and SortingFilters and/or FieldFilters
145
+ let datasetFilters: GraphqlDatasetFilter = {};
146
+
147
+ // only add pagination if it's enabled in the grid options
148
+ if (this._gridOptions.enablePagination !== false) {
149
+ datasetFilters = {};
150
+
151
+ if (this.options.useCursor && this.options.paginationOptions) {
152
+ datasetFilters = { ...this.options.paginationOptions };
153
+ }
154
+ else {
155
+ const paginationOptions = this.options?.paginationOptions;
156
+ datasetFilters.first = this.options?.paginationOptions?.first ?? this.pagination?.pageSize ?? this.defaultPaginationOptions.first;
157
+ datasetFilters.offset = paginationOptions?.hasOwnProperty('offset') ? +(paginationOptions as any)['offset'] : 0;
158
+ }
159
+ }
160
+
161
+ if (this.options.sortingOptions && Array.isArray(this.options.sortingOptions) && this.options.sortingOptions.length > 0) {
162
+ // orderBy: [{ field:x, direction: 'ASC' }]
163
+ datasetFilters.orderBy = this.options.sortingOptions;
164
+ }
165
+ if (this.options.filteringOptions && Array.isArray(this.options.filteringOptions) && this.options.filteringOptions.length > 0) {
166
+ // filterBy: [{ field: date, operator: '>', value: '2000-10-10' }]
167
+ datasetFilters.filterBy = this.options.filteringOptions;
168
+ }
169
+ if (this.options.addLocaleIntoQuery) {
170
+ // first: 20, ... locale: "en-CA"
171
+ datasetFilters.locale = this._gridOptions.translater?.getCurrentLanguage() || this._gridOptions.locale || 'en';
172
+ }
173
+ if (this.options.extraQueryArguments) {
174
+ // first: 20, ... userId: 123
175
+ for (const queryArgument of this.options.extraQueryArguments) {
176
+ (datasetFilters as any)[queryArgument.field] = queryArgument.value;
177
+ }
178
+ }
179
+
180
+ // with pagination:: query { users(first: 20, offset: 0, orderBy: [], filterBy: []) { totalCount: 100, nodes: { _columns_ }}}
181
+ // without pagination:: query { users(orderBy: [], filterBy: []) { _columns_ }}
182
+ datasetQb.filter(datasetFilters);
183
+ queryQb.find(datasetQb);
184
+
185
+ const enumSearchProperties = ['direction:', 'field:', 'operator:'];
186
+ return this.trimDoubleQuotesOnEnumField(queryQb.toString(), enumSearchProperties, this.options.keepArgumentFieldDoubleQuotes || false);
187
+ }
188
+
189
+ /**
190
+ * From an input array of strings, we want to build a GraphQL query string.
191
+ * The process has to take the dot notation and parse it into a valid GraphQL query
192
+ * Following this SO answer https://stackoverflow.com/a/47705476/1212166
193
+ *
194
+ * INPUT::
195
+ * ['firstName', 'lastName', 'billing.address.street', 'billing.address.zip']
196
+ * OUTPUT::
197
+ * firstName, lastName, billing{address{street, zip}}
198
+ * @param inputArray
199
+ */
200
+ buildFilterQuery(inputArray: string[]) {
201
+
202
+ const set = (o: any = {}, a: any) => {
203
+ const k = a.shift();
204
+ o[k] = a.length ? set(o[k] ?? {}, a) : null;
205
+ return o;
206
+ };
207
+
208
+ const output = inputArray.reduce((o: any, a: string) => set(o, a.split('.')), {});
209
+
210
+ return JSON.stringify(output)
211
+ .replace(/\"|\:|null/g, '')
212
+ .replace(/^\{/, '')
213
+ .replace(/\}$/, '');
214
+ }
215
+
216
+ clearFilters() {
217
+ this._currentFilters = [];
218
+ this.updateOptions({ filteringOptions: [] });
219
+ }
220
+
221
+ clearSorters() {
222
+ this._currentSorters = [];
223
+ this.updateOptions({ sortingOptions: [] });
224
+ }
225
+
226
+ /**
227
+ * Get an initialization of Pagination options
228
+ * @return Pagination Options
229
+ */
230
+ getInitPaginationOptions(): GraphqlDatasetFilter {
231
+ const paginationFirst = this.pagination ? this.pagination.pageSize : DEFAULT_ITEMS_PER_PAGE;
232
+ return this.options?.useCursor
233
+ ? { first: paginationFirst }
234
+ : { first: paginationFirst, offset: 0 };
235
+ }
236
+
237
+ /** Get the GraphQL dataset name */
238
+ getDatasetName(): string {
239
+ return this.options?.datasetName || '';
240
+ }
241
+
242
+ /** Get the Filters that are currently used by the grid */
243
+ getCurrentFilters(): ColumnFilters | CurrentFilter[] {
244
+ return this._currentFilters;
245
+ }
246
+
247
+ /** Get the Pagination that is currently used by the grid */
248
+ getCurrentPagination(): CurrentPagination | null {
249
+ return this._currentPagination;
250
+ }
251
+
252
+ /** Get the Sorters that are currently used by the grid */
253
+ getCurrentSorters(): CurrentSorter[] {
254
+ return this._currentSorters;
255
+ }
256
+
257
+ /*
258
+ * Reset the pagination options
259
+ */
260
+ resetPaginationOptions() {
261
+ let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption;
262
+
263
+ if (this.options?.useCursor) {
264
+ paginationOptions = this.getInitPaginationOptions();
265
+ } else {
266
+ // first, last, offset
267
+ paginationOptions = ((this.options && this.options.paginationOptions) || this.getInitPaginationOptions()) as GraphqlPaginationOption;
268
+ (paginationOptions as GraphqlPaginationOption).offset = 0;
269
+ }
270
+
271
+ // save current pagination as Page 1 and page size as "first" set size
272
+ this._currentPagination = {
273
+ pageNumber: 1,
274
+ pageSize: paginationOptions.first || DEFAULT_PAGE_SIZE
275
+ };
276
+
277
+ // unless user specifically set "enablePagination" to False, we'll update pagination options in every other cases
278
+ if (this._gridOptions && (this._gridOptions.enablePagination || !this._gridOptions.hasOwnProperty('enablePagination'))) {
279
+ this.updateOptions({ paginationOptions });
280
+ }
281
+ }
282
+
283
+ updateOptions(serviceOptions?: Partial<GraphqlServiceOption>) {
284
+ this.options = { ...this.options, ...serviceOptions } as GraphqlServiceOption;
285
+ }
286
+
287
+ /*
288
+ * FILTERING
289
+ */
290
+ processOnFilterChanged(_event: Event | undefined, args: FilterChangedArgs): string {
291
+ const gridOptions: GridOption = this._gridOptions;
292
+ const backendApi = gridOptions.backendServiceApi;
293
+
294
+ if (backendApi === undefined) {
295
+ throw new Error('Something went wrong in the GraphqlService, "backendServiceApi" is not initialized');
296
+ }
297
+
298
+ // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter)
299
+ this._currentFilters = this.castFilterToColumnFilters(args.columnFilters);
300
+
301
+ if (!args || !args.grid) {
302
+ throw new Error('Something went wrong when trying create the GraphQL Backend Service, it seems that "args" is not populated correctly');
303
+ }
304
+
305
+ // loop through all columns to inspect filters & set the query
306
+ this.updateFilters(args.columnFilters, false);
307
+
308
+ this.resetPaginationOptions();
309
+ return this.buildQuery();
310
+ }
311
+
312
+ /*
313
+ * PAGINATION
314
+ * With cursor, the query can have 4 arguments (first, after, last, before), for example:
315
+ * users (first:20, after:"YXJyYXljb25uZWN0aW9uOjM=") {
316
+ * totalCount
317
+ * pageInfo {
318
+ * hasNextPage
319
+ * hasPreviousPage
320
+ * endCursor
321
+ * startCursor
322
+ * }
323
+ * edges {
324
+ * cursor
325
+ * node {
326
+ * name
327
+ * gender
328
+ * }
329
+ * }
330
+ * }
331
+ * Without cursor, the query can have 3 arguments (first, last, offset), for example:
332
+ * users (first:20, offset: 10) {
333
+ * totalCount
334
+ * nodes {
335
+ * name
336
+ * gender
337
+ * }
338
+ * }
339
+ */
340
+ processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | (PaginationCursorChangedArgs & PaginationChangedArgs)): string {
341
+ const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE));
342
+
343
+ // if first/last defined on args, then it is a cursor based pagination change
344
+ 'first' in args || 'last' in args
345
+ ? this.updatePagination(args.newPage, pageSize, args)
346
+ : this.updatePagination(args.newPage, pageSize);
347
+
348
+
349
+ // build the GraphQL query which we will use in the WebAPI callback
350
+ return this.buildQuery();
351
+ }
352
+
353
+ /*
354
+ * SORTING
355
+ * we will use sorting as per a Facebook suggestion on a Github issue (with some small changes)
356
+ * https://github.com/graphql/graphql-relay-js/issues/20#issuecomment-220494222
357
+ *
358
+ * users (first: 20, offset: 10, orderBy: [{field: lastName, direction: ASC}, {field: firstName, direction: DESC}]) {
359
+ * totalCount
360
+ * nodes {
361
+ * name
362
+ * gender
363
+ * }
364
+ * }
365
+ */
366
+ processOnSortChanged(_event: Event | undefined, args: SingleColumnSort | MultiColumnSort): string {
367
+ const sortColumns = (args.multiColumnSort) ? (args as MultiColumnSort).sortCols : new Array({ columnId: (args as ColumnSort).sortCol?.id ?? '', sortCol: (args as ColumnSort).sortCol, sortAsc: (args as ColumnSort).sortAsc });
368
+
369
+ // loop through all columns to inspect sorters & set the query
370
+ this.updateSorters(sortColumns);
371
+
372
+ // build the GraphQL query which we will use in the WebAPI callback
373
+ return this.buildQuery();
374
+ }
375
+
376
+ /**
377
+ * Update column filters by looping through all columns to inspect filters & update backend service filteringOptions
378
+ * @param columnFilters
379
+ */
380
+ updateFilters(columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPresetOrDynamically: boolean) {
381
+ const searchByArray: GraphqlFilteringOption[] = [];
382
+ let searchValue: string | string[];
383
+
384
+ // on filter preset load, we need to keep current filters
385
+ if (isUpdatedByPresetOrDynamically) {
386
+ this._currentFilters = this.castFilterToColumnFilters(columnFilters);
387
+ }
388
+
389
+ for (const columnId in columnFilters) {
390
+ if (columnFilters.hasOwnProperty(columnId)) {
391
+ const columnFilter = (columnFilters as any)[columnId];
392
+
393
+ // if user defined some "presets", then we need to find the filters from the column definitions instead
394
+ let columnDef: Column | undefined;
395
+ if (isUpdatedByPresetOrDynamically && Array.isArray(this._columnDefinitions)) {
396
+ columnDef = this._columnDefinitions.find((column: Column) => column.id === columnFilter.columnId);
397
+ } else {
398
+ columnDef = columnFilter.columnDef;
399
+ }
400
+ if (!columnDef) {
401
+ throw new Error('[GraphQL Service]: Something went wrong in trying to get the column definition of the specified filter (or preset filters). Did you make a typo on the filter columnId?');
402
+ }
403
+
404
+ let fieldName = columnDef.filter?.queryField || columnDef.queryFieldFilter || columnDef.queryField || columnDef.field || columnDef.name || '';
405
+ if (fieldName instanceof HTMLElement) {
406
+ fieldName = stripTags(fieldName.innerHTML);
407
+ }
408
+ const fieldType = columnDef.type || FieldType.string;
409
+ let searchTerms = columnFilter?.searchTerms ?? [];
410
+ let fieldSearchValue = (Array.isArray(searchTerms) && searchTerms.length === 1) ? searchTerms[0] : '';
411
+ if (typeof fieldSearchValue === 'undefined') {
412
+ fieldSearchValue = '';
413
+ }
414
+
415
+ if (!fieldName) {
416
+ 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").`);
417
+ }
418
+
419
+ if (this.options?.useVerbatimSearchTerms || columnFilter.verbatimSearchTerms) {
420
+ searchByArray.push({ field: fieldName, operator: columnFilter.operator, value: JSON.stringify(columnFilter.searchTerms) });
421
+ continue;
422
+ }
423
+
424
+ fieldSearchValue = (fieldSearchValue === undefined || fieldSearchValue === null) ? '' : `${fieldSearchValue}`; // make sure it's a string
425
+
426
+ // run regex to find possible filter operators unless the user disabled the feature
427
+ const autoParseInputFilterOperator = columnDef.autoParseInputFilterOperator ?? this._gridOptions.autoParseInputFilterOperator;
428
+ const matches = autoParseInputFilterOperator !== false
429
+ ? fieldSearchValue.match(/^([<>!=\*]{0,2})(.*[^<>!=\*])([\*]?)$/) // group 1: Operator, 2: searchValue, 3: last char is '*' (meaning starts with, ex.: abc*)
430
+ : [fieldSearchValue, '', fieldSearchValue, '']; // when parsing is disabled, we'll only keep the search value in the index 2 to make it easy for code reuse
431
+
432
+ let operator: OperatorString = columnFilter.operator || matches?.[1] || '';
433
+ searchValue = matches?.[2] || '';
434
+ const lastValueChar = matches?.[3] || (operator === '*z' ? '*' : '');
435
+
436
+ // no need to query if search value is empty
437
+ if (fieldName && searchValue === '' && searchTerms.length === 0) {
438
+ continue;
439
+ }
440
+
441
+ if (Array.isArray(searchTerms) && searchTerms.length === 1 && typeof searchTerms[0] === 'string' && searchTerms[0].indexOf('..') >= 0) {
442
+ if (operator !== OperatorType.rangeInclusive && operator !== OperatorType.rangeExclusive) {
443
+ operator = this._gridOptions.defaultFilterRangeOperator ?? OperatorType.rangeInclusive;
444
+ }
445
+ searchTerms = searchTerms[0].split('..', 2);
446
+ if (searchTerms[0] === '') {
447
+ operator = operator === OperatorType.rangeInclusive ? '<=' : operator === OperatorType.rangeExclusive ? '<' : operator;
448
+ searchTerms = searchTerms.slice(1);
449
+ searchValue = searchTerms[0];
450
+ } else if (searchTerms[1] === '') {
451
+ operator = operator === OperatorType.rangeInclusive ? '>=' : operator === OperatorType.rangeExclusive ? '>' : operator;
452
+ searchTerms = searchTerms.slice(0, 1);
453
+ searchValue = searchTerms[0];
454
+ }
455
+ }
456
+
457
+ if (typeof searchValue === 'string') {
458
+ if (operator === '*' || operator === 'a*' || operator === '*z' || lastValueChar === '*') {
459
+ operator = ((operator === '*' || operator === '*z') ? 'EndsWith' : 'StartsWith') as OperatorString;
460
+ }
461
+ }
462
+
463
+ // if we didn't find an Operator but we have a Column Operator inside the Filter (DOM Element), we should use its default Operator
464
+ // multipleSelect is "IN", while singleSelect is "EQ", else don't map any operator
465
+ if (!operator && columnDef.filter && columnDef.filter.operator) {
466
+ operator = columnDef.filter.operator;
467
+ }
468
+
469
+ // No operator and 2 search terms should lead to default range operator.
470
+ if (!operator && Array.isArray(searchTerms) && searchTerms.length === 2 && searchTerms[0] && searchTerms[1]) {
471
+ operator = this._gridOptions.defaultFilterRangeOperator as OperatorString;
472
+ }
473
+
474
+ // Range with 1 searchterm should lead to equals for a date field.
475
+ if ((operator === OperatorType.rangeInclusive || OperatorType.rangeExclusive) && Array.isArray(searchTerms) && searchTerms.length === 1 && fieldType === FieldType.date) {
476
+ operator = OperatorType.equal;
477
+ }
478
+
479
+ // Normalize all search values
480
+ searchValue = this.normalizeSearchValue(fieldType, searchValue);
481
+ if (Array.isArray(searchTerms)) {
482
+ searchTerms.forEach((_part, index) => {
483
+ searchTerms[index] = this.normalizeSearchValue(fieldType, searchTerms[index]);
484
+ });
485
+ }
486
+
487
+ // when having more than 1 search term (we need to create a CSV string for GraphQL "IN" or "NOT IN" filter search)
488
+ if (searchTerms && searchTerms.length > 1 && (operator === 'IN' || operator === 'NIN' || operator === 'NOT_IN')) {
489
+ searchValue = searchTerms.join(',');
490
+ } else if (searchTerms && searchTerms.length === 2 && (operator === OperatorType.rangeExclusive || operator === OperatorType.rangeInclusive)) {
491
+ searchByArray.push({ field: fieldName, operator: (operator === OperatorType.rangeInclusive ? 'GE' : 'GT'), value: searchTerms[0] });
492
+ searchByArray.push({ field: fieldName, operator: (operator === OperatorType.rangeInclusive ? 'LE' : 'LT'), value: searchTerms[1] });
493
+ continue;
494
+ }
495
+
496
+ // if we still don't have an operator find the proper Operator to use by it's field type
497
+ if (!operator) {
498
+ operator = mapOperatorByFieldType(fieldType);
499
+ }
500
+
501
+ // build the search array
502
+ searchByArray.push({ field: fieldName, operator: mapOperatorType(operator), value: searchValue });
503
+ }
504
+ }
505
+
506
+ // update the service options with filters for the buildQuery() to work later
507
+ this.updateOptions({ filteringOptions: searchByArray });
508
+ }
509
+
510
+ /**
511
+ * Update the pagination component with it's new page number and size.
512
+ * @param {Number} newPage
513
+ * @param {Number} pageSize
514
+ * @param {*} [cursorArgs] these should be supplied when using cursor based pagination
515
+ */
516
+ updatePagination(newPage: number, pageSize: number, cursorArgs?: PaginationCursorChangedArgs) {
517
+ this._currentPagination = {
518
+ pageNumber: newPage,
519
+ pageSize
520
+ };
521
+
522
+ let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {};
523
+ if (this.options?.useCursor) {
524
+ // use cursor based pagination
525
+ // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments,
526
+ // but still handle the case where it's not (can happen when initial configuration not pre-configured (automatically corrects itself next setCursorPageInfo() call))
527
+ if (cursorArgs && cursorArgs instanceof Object) {
528
+ // remove pageSize and newPage from cursorArgs, otherwise they get put on the query input string
529
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow
530
+ const { pageSize, newPage, ...cursorPaginationOptions } = cursorArgs;
531
+ paginationOptions = cursorPaginationOptions;
532
+ } else {
533
+ paginationOptions = { first: pageSize };
534
+ }
535
+ } else {
536
+ // use offset based pagination
537
+ paginationOptions = {
538
+ first: pageSize,
539
+ offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0
540
+ };
541
+ }
542
+
543
+ this.updateOptions({ paginationOptions });
544
+ }
545
+
546
+ /**
547
+ * Update all Sorting by looping through all columns to inspect sorters & update backend service sortingOptions
548
+ */
549
+ updateSorters(sortColumns?: ColumnSort[], presetSorters?: CurrentSorter[]) {
550
+ let currentSorters: CurrentSorter[] = [];
551
+ const graphqlSorters: GraphqlSortingOption[] = [];
552
+
553
+ if (!sortColumns && presetSorters) {
554
+ // make the presets the current sorters, also make sure that all direction are in uppercase for GraphQL
555
+ currentSorters = presetSorters;
556
+ currentSorters.forEach((sorter) => sorter.direction = sorter.direction.toUpperCase() as SortDirectionString);
557
+
558
+ // display the correct sorting icons on the UI, for that it requires (columnId, sortAsc) properties
559
+ const tmpSorterArray = currentSorters.map((sorter) => {
560
+ const columnDef = this._columnDefinitions?.find((column: Column) => column.id === sorter.columnId);
561
+
562
+ graphqlSorters.push({
563
+ field: columnDef ? ((columnDef.queryFieldSorter || columnDef.queryField || columnDef.field) + '') : (sorter.columnId + ''),
564
+ direction: sorter.direction
565
+ });
566
+
567
+ // return only the column(s) found in the Column Definitions ELSE null
568
+ if (columnDef) {
569
+ return {
570
+ columnId: sorter.columnId,
571
+ sortAsc: sorter.direction.toUpperCase() === SortDirection.ASC
572
+ };
573
+ }
574
+ return null;
575
+ }) as { columnId: string | number; sortAsc: boolean; }[] | null;
576
+
577
+ // set the sort icons, but also make sure to filter out null values (that happens when columnDef is not found)
578
+ if (Array.isArray(tmpSorterArray) && this._grid) {
579
+ this._grid.setSortColumns(tmpSorterArray.filter(sorter => sorter) || []);
580
+ }
581
+ } else if (sortColumns && !presetSorters) {
582
+ // build the orderBy array, it could be multisort, example
583
+ // orderBy:[{field: lastName, direction: ASC}, {field: firstName, direction: DESC}]
584
+ if (Array.isArray(sortColumns) && sortColumns.length > 0) {
585
+ for (const column of sortColumns) {
586
+ if (column && column.sortCol) {
587
+ currentSorters.push({
588
+ columnId: column.sortCol.id + '',
589
+ direction: column.sortAsc ? SortDirection.ASC : SortDirection.DESC
590
+ });
591
+
592
+ const fieldName = (column.sortCol.queryFieldSorter || column.sortCol.queryField || column.sortCol.field || '') + '';
593
+ if (fieldName) {
594
+ graphqlSorters.push({
595
+ field: fieldName,
596
+ direction: column.sortAsc ? SortDirection.ASC : SortDirection.DESC
597
+ });
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+
604
+ // keep current Sorters and update the service options with the new sorting
605
+ this._currentSorters = currentSorters;
606
+ this.updateOptions({ sortingOptions: graphqlSorters });
607
+ }
608
+
609
+ /**
610
+ * A function which takes an input string and removes double quotes only
611
+ * on certain fields are identified as GraphQL enums (except fields with dot notation)
612
+ * For example let say we identified ("direction:", "sort") as word which are GraphQL enum fields
613
+ * then the result will be:
614
+ * FROM
615
+ * query { users (orderBy:[{field:"firstName", direction:"ASC"} }]) }
616
+ * TO
617
+ * query { users (orderBy:[{field: firstName, direction: ASC}})}
618
+ *
619
+ * EXCEPTIONS (fields with dot notation "." which are inside a "field:")
620
+ * these fields will keep double quotes while everything else will be stripped of double quotes
621
+ * query { users (orderBy:[{field:"billing.street.name", direction: "ASC"} }
622
+ * TO
623
+ * query { users (orderBy:[{field:"billing.street.name", direction: ASC}}
624
+ * @param inputStr input string
625
+ * @param enumSearchWords array of enum words to filter
626
+ * @param keepArgumentFieldDoubleQuotes - do we keep field double quotes? (i.e.: field: "user.name")
627
+ * @returns outputStr output string
628
+ */
629
+ trimDoubleQuotesOnEnumField(inputStr: string, enumSearchWords: string[], keepArgumentFieldDoubleQuotes: boolean) {
630
+ const patternWordInQuotes = `\s?((field:\s*)?".*?")`;
631
+ let patternRegex = enumSearchWords.join(patternWordInQuotes + '|');
632
+ patternRegex += patternWordInQuotes; // the last one should also have the pattern but without the pipe "|"
633
+ // example with (field: & direction:): /field:s?(".*?")|direction:s?(".*?")/
634
+ const reg = new RegExp(patternRegex, 'g');
635
+
636
+ return inputStr.replace(reg, group1 => {
637
+ // remove double quotes except when the string starts with a "field:"
638
+ let removeDoubleQuotes = true;
639
+ if (group1.startsWith('field:') && keepArgumentFieldDoubleQuotes) {
640
+ removeDoubleQuotes = false;
641
+ }
642
+ const rep = removeDoubleQuotes ? group1.replace(/"/g, '') : group1;
643
+ return rep;
644
+ });
645
+ }
646
+
647
+ //
648
+ // protected functions
649
+ // -------------------
650
+ /**
651
+ * Cast provided filters (could be in multiple formats) into an array of CurrentFilter
652
+ * @param columnFilters
653
+ */
654
+ protected castFilterToColumnFilters(columnFilters: ColumnFilters | CurrentFilter[]): CurrentFilter[] {
655
+ // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter)
656
+ const filtersArray: ColumnFilter[] = (typeof columnFilters === 'object') ? Object.keys(columnFilters).map(key => (columnFilters as any)[key]) : columnFilters;
657
+
658
+ if (!Array.isArray(filtersArray)) {
659
+ return [];
660
+ }
661
+
662
+ return filtersArray.map((filter) => {
663
+ const tmpFilter: CurrentFilter = { columnId: filter.columnId || '' };
664
+ if (filter.operator) {
665
+ tmpFilter.operator = filter.operator;
666
+ }
667
+ if (filter.targetSelector) {
668
+ tmpFilter.targetSelector = filter.targetSelector;
669
+ }
670
+ if (Array.isArray(filter.searchTerms)) {
671
+ tmpFilter.searchTerms = filter.searchTerms;
672
+ }
673
+ return tmpFilter;
674
+ });
675
+ }
676
+
677
+ /** Normalizes the search value according to field type. */
678
+ protected normalizeSearchValue(fieldType: typeof FieldType[keyof typeof FieldType], searchValue: any): any {
679
+ switch (fieldType) {
680
+ case FieldType.date:
681
+ case FieldType.string:
682
+ case FieldType.text:
683
+ case FieldType.readonly:
684
+ if (typeof searchValue === 'string') {
685
+ // escape single quotes by doubling them
686
+ searchValue = searchValue.replace(/'/g, `''`);
687
+ }
688
+ break;
689
+ case FieldType.integer:
690
+ case FieldType.number:
691
+ case FieldType.float:
692
+ if (typeof searchValue === 'string') {
693
+ // Parse a valid decimal from the string.
694
+
695
+ // Replace double dots with single dots
696
+ searchValue = searchValue.replace(/\.\./g, '.');
697
+ // Remove a trailing dot
698
+ searchValue = searchValue.replace(/\.+$/g, '');
699
+ // Prefix a leading dot with 0
700
+ searchValue = searchValue.replace(/^\.+/g, '0.');
701
+ // Prefix leading dash dot with -0.
702
+ searchValue = searchValue.replace(/^\-+\.+/g, '-0.');
703
+ // Remove any non valid decimal characters from the search string
704
+ searchValue = searchValue.replace(/(?!^\-)[^\d\.]/g, '');
705
+
706
+ // if nothing left, search for 0
707
+ if (searchValue === '' || searchValue === '-') {
708
+ searchValue = '0';
709
+ }
710
+ }
711
+ break;
712
+ }
713
+
714
+ return searchValue;
715
+ }
716
+ }