@slickgrid-universal/odata 4.0.2 → 4.1.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,690 @@
1
+ import type {
2
+ // enums/interfaces
3
+ BackendService,
4
+ Column,
5
+ ColumnFilter,
6
+ ColumnFilters,
7
+ ColumnSort,
8
+ CurrentFilter,
9
+ CurrentPagination,
10
+ CurrentSorter,
11
+ FilterChangedArgs,
12
+ GridOption,
13
+ MultiColumnSort,
14
+ Pagination,
15
+ PaginationChangedArgs,
16
+ SortDirectionString,
17
+ OperatorString,
18
+ SearchTerm,
19
+ SharedService,
20
+ SingleColumnSort,
21
+ SlickGrid,
22
+ } from '@slickgrid-universal/common';
23
+ import {
24
+ CaseType,
25
+ FieldType,
26
+ mapOperatorByFieldType,
27
+ OperatorType,
28
+ parseUtcDate,
29
+ SortDirection,
30
+ } from '@slickgrid-universal/common';
31
+ import { stripTags, titleCase } from '@slickgrid-universal/utils';
32
+ import { OdataQueryBuilderService } from './odataQueryBuilder.service';
33
+ import { OdataOption, OdataSortingOption } from '../interfaces/index';
34
+
35
+ const DEFAULT_ITEMS_PER_PAGE = 25;
36
+ const DEFAULT_PAGE_SIZE = 20;
37
+
38
+ export class GridOdataService implements BackendService {
39
+ protected _currentFilters: CurrentFilter[] = [];
40
+ protected _currentPagination: CurrentPagination | null = null;
41
+ protected _currentSorters: CurrentSorter[] = [];
42
+ protected _columnDefinitions: Column[] = [];
43
+ protected _grid: SlickGrid | undefined;
44
+ protected _odataService: OdataQueryBuilderService;
45
+ options?: Partial<OdataOption>;
46
+ pagination: Pagination | undefined;
47
+ defaultOptions: OdataOption = {
48
+ top: DEFAULT_ITEMS_PER_PAGE,
49
+ orderBy: '',
50
+ caseType: CaseType.pascalCase
51
+ };
52
+
53
+ /** Getter for the Column Definitions */
54
+ get columnDefinitions() {
55
+ return this._columnDefinitions;
56
+ }
57
+
58
+ /** Getter for the Odata Service */
59
+ get odataService() {
60
+ return this._odataService;
61
+ }
62
+
63
+ /** Getter for the Grid Options pulled through the Grid Object */
64
+ protected get _gridOptions(): GridOption {
65
+ return this._grid?.getOptions() ?? {} as GridOption;
66
+ }
67
+
68
+ constructor() {
69
+ this._odataService = new OdataQueryBuilderService();
70
+ }
71
+
72
+ init(serviceOptions?: Partial<OdataOption>, pagination?: Pagination, grid?: SlickGrid, sharedService?: SharedService): void {
73
+ this._grid = grid;
74
+ const mergedOptions = { ...this.defaultOptions, ...serviceOptions };
75
+
76
+ // unless user specifically set "enablePagination" to False, we'll add "top" property for the pagination in every other cases
77
+ if (this._gridOptions && !this._gridOptions.enablePagination) {
78
+ // save current pagination as Page 1 and page size as "top"
79
+ this._odataService.options = { ...mergedOptions, top: undefined };
80
+ this._currentPagination = null;
81
+ } else {
82
+ const topOption = (pagination && pagination.pageSize) ? pagination.pageSize : this.defaultOptions.top;
83
+ this._odataService.options = { ...mergedOptions, top: topOption };
84
+ this._currentPagination = {
85
+ pageNumber: 1,
86
+ pageSize: this._odataService.options.top || this.defaultOptions.top || DEFAULT_PAGE_SIZE
87
+ };
88
+ }
89
+
90
+ this.options = this._odataService.options;
91
+ this.pagination = pagination;
92
+
93
+ if (grid?.getColumns) {
94
+ const tmpColumnDefinitions = sharedService?.allColumns ?? grid.getColumns() ?? [];
95
+ this._columnDefinitions = tmpColumnDefinitions.filter((column: Column) => !column.excludeFromQuery);
96
+ }
97
+
98
+ this._odataService.columnDefinitions = this._columnDefinitions;
99
+ this._odataService.datasetIdPropName = this._gridOptions.datasetIdPropertyName || 'id';
100
+ }
101
+
102
+ buildQuery(): string {
103
+ return this._odataService.buildQuery();
104
+ }
105
+
106
+ postProcess(processResult: any): void {
107
+ const odataVersion = this._odataService?.options?.version ?? 2;
108
+
109
+ if (this.pagination && this._odataService?.options?.enableCount) {
110
+ const countExtractor = this._odataService?.options?.countExtractor ??
111
+ odataVersion >= 4 ? (r: any) => r?.['@odata.count'] :
112
+ odataVersion === 3 ? (r: any) => r?.['__count'] :
113
+ (r: any) => r?.d?.['__count'];
114
+ const count = countExtractor(processResult);
115
+ if (typeof count === 'number') {
116
+ this.pagination.totalItems = count;
117
+ }
118
+ }
119
+
120
+ if (this._odataService?.options?.enableExpand) {
121
+ const datasetExtractor = this._odataService?.options?.datasetExtractor ??
122
+ odataVersion >= 4 ? (r: any) => r?.value :
123
+ odataVersion === 3 ? (r: any) => r?.results :
124
+ (r: any) => r?.d?.results;
125
+ const dataset = datasetExtractor(processResult);
126
+ if (Array.isArray(dataset)) {
127
+ // Flatten navigation fields (fields containing /) in the dataset (regardless of enableExpand).
128
+ // E.g. given columndefinition 'product/name' and dataset [{id: 1,product:{'name':'flowers'}}], then flattens to [{id:1,'product/name':'flowers'}]
129
+ const navigationFields = new Set(this._columnDefinitions.flatMap(x => x.fields ?? [x.field]).filter(x => x.includes('/')));
130
+ if (navigationFields.size > 0) {
131
+ const navigations = new Set<string>();
132
+ for (const item of dataset) {
133
+ for (const field of navigationFields) {
134
+ const names = field.split('/');
135
+ const navigation = names[0];
136
+ navigations.add(navigation);
137
+
138
+ let val = item[navigation];
139
+ for (let i = 1; i < names.length; i++) {
140
+ const mappedName = names[i];
141
+ if (val && typeof val === 'object' && mappedName in val) {
142
+ val = val[mappedName];
143
+ }
144
+ }
145
+
146
+ item[field] = val;
147
+ }
148
+
149
+ // Remove navigation objects from the dataset to free memory and make sure we never work with them.
150
+ for (const navigation of navigations) {
151
+ if (typeof item[navigation] === 'object') {
152
+ delete item[navigation];
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ clearFilters() {
162
+ this._currentFilters = [];
163
+ this.updateFilters([]);
164
+ }
165
+
166
+ clearSorters() {
167
+ this._currentSorters = [];
168
+ this.updateSorters([]);
169
+ }
170
+
171
+ updateOptions(serviceOptions?: Partial<OdataOption>) {
172
+ this.options = { ...this.options, ...serviceOptions };
173
+ this._odataService.options = this.options;
174
+ }
175
+
176
+ removeColumnFilter(fieldName: string): void {
177
+ this._odataService.removeColumnFilter(fieldName);
178
+ }
179
+
180
+ /** Get the Filters that are currently used by the grid */
181
+ getCurrentFilters(): CurrentFilter[] {
182
+ return this._currentFilters;
183
+ }
184
+
185
+ /** Get the Pagination that is currently used by the grid */
186
+ getCurrentPagination(): CurrentPagination | null {
187
+ return this._currentPagination;
188
+ }
189
+
190
+ /** Get the Sorters that are currently used by the grid */
191
+ getCurrentSorters(): CurrentSorter[] {
192
+ return this._currentSorters;
193
+ }
194
+
195
+ /**
196
+ * Mapper for mathematical operators (ex.: <= is "le", > is "gt")
197
+ * @param string operator
198
+ * @returns string map
199
+ */
200
+ mapOdataOperator(operator: string) {
201
+ let map = '';
202
+ switch (operator) {
203
+ case '<':
204
+ map = 'lt';
205
+ break;
206
+ case '<=':
207
+ map = 'le';
208
+ break;
209
+ case '>':
210
+ map = 'gt';
211
+ break;
212
+ case '>=':
213
+ map = 'ge';
214
+ break;
215
+ case '<>':
216
+ case '!=':
217
+ map = 'ne';
218
+ break;
219
+ case '=':
220
+ case '==':
221
+ default:
222
+ map = 'eq';
223
+ break;
224
+ }
225
+
226
+ return map;
227
+ }
228
+
229
+ /*
230
+ * Reset the pagination options
231
+ */
232
+ resetPaginationOptions() {
233
+ this._odataService.updateOptions({
234
+ skip: 0
235
+ });
236
+ }
237
+
238
+ saveColumnFilter(fieldName: string, value: string, terms?: SearchTerm[]) {
239
+ this._odataService.saveColumnFilter(fieldName, value, terms);
240
+ }
241
+
242
+ /*
243
+ * FILTERING
244
+ */
245
+ processOnFilterChanged(_event: Event | undefined, args: FilterChangedArgs): string {
246
+ const gridOptions: GridOption = this._gridOptions;
247
+ const backendApi = gridOptions.backendServiceApi;
248
+
249
+ if (backendApi === undefined) {
250
+ throw new Error('Something went wrong in the GridOdataService, "backendServiceApi" is not initialized');
251
+ }
252
+
253
+ // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter)
254
+ this._currentFilters = this.castFilterToColumnFilters(args.columnFilters);
255
+
256
+ if (!args || !args.grid) {
257
+ throw new Error('Something went wrong when trying create the GridOdataService, it seems that "args" is not populated correctly');
258
+ }
259
+
260
+ // loop through all columns to inspect filters & set the query
261
+ this.updateFilters(args.columnFilters);
262
+
263
+ this.resetPaginationOptions();
264
+ return this._odataService.buildQuery();
265
+ }
266
+
267
+ /*
268
+ * PAGINATION
269
+ */
270
+ processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs) {
271
+ const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE));
272
+ this.updatePagination(args.newPage, pageSize);
273
+
274
+ // build the OData query which we will use in the WebAPI callback
275
+ return this._odataService.buildQuery();
276
+ }
277
+
278
+ /*
279
+ * SORTING
280
+ */
281
+ processOnSortChanged(_event: Event | undefined, args: SingleColumnSort | MultiColumnSort) {
282
+ 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 });
283
+
284
+ // loop through all columns to inspect sorters & set the query
285
+ this.updateSorters(sortColumns);
286
+
287
+ // build the OData query which we will use in the WebAPI callback
288
+ return this._odataService.buildQuery();
289
+ }
290
+
291
+ /**
292
+ * loop through all columns to inspect filters & update backend service filters
293
+ * @param columnFilters
294
+ */
295
+ updateFilters(columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPresetOrDynamically?: boolean) {
296
+ let searchBy = '';
297
+ const searchByArray: string[] = [];
298
+ const odataVersion = this._odataService?.options?.version ?? 2;
299
+
300
+ // on filter preset load, we need to keep current filters
301
+ if (isUpdatedByPresetOrDynamically) {
302
+ this._currentFilters = this.castFilterToColumnFilters(columnFilters);
303
+ }
304
+
305
+ // loop through all columns to inspect filters
306
+ for (const columnId in columnFilters) {
307
+ if (columnFilters.hasOwnProperty(columnId)) {
308
+ const columnFilter = (columnFilters as any)[columnId];
309
+
310
+ // if user defined some "presets", then we need to find the filters from the column definitions instead
311
+ let columnDef: Column | undefined;
312
+ if (isUpdatedByPresetOrDynamically && Array.isArray(this._columnDefinitions)) {
313
+ columnDef = this._columnDefinitions.find((column: Column) => column.id === columnFilter.columnId);
314
+ } else {
315
+ columnDef = columnFilter.columnDef;
316
+ }
317
+ if (!columnDef) {
318
+ throw new Error('[GridOData 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?');
319
+ }
320
+
321
+ let fieldName = columnDef.filter?.queryField || columnDef.queryFieldFilter || columnDef.queryField || columnDef.field || columnDef.name || '';
322
+ if (fieldName instanceof HTMLElement) {
323
+ fieldName = stripTags(fieldName.innerHTML);
324
+ }
325
+ const fieldType = columnDef.type || FieldType.string;
326
+ let searchTerms = (columnFilter && columnFilter.searchTerms ? [...columnFilter.searchTerms] : null) || [];
327
+ let fieldSearchValue = (Array.isArray(searchTerms) && searchTerms.length === 1) ? searchTerms[0] : '';
328
+ if (typeof fieldSearchValue === 'undefined') {
329
+ fieldSearchValue = '';
330
+ }
331
+
332
+ if (!fieldName) {
333
+ 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").`);
334
+ }
335
+
336
+ if (this._odataService.options.useVerbatimSearchTerms || columnFilter.verbatimSearchTerms) {
337
+ searchByArray.push(`${fieldName} ${columnFilter.operator} ${JSON.stringify(columnFilter.searchTerms)}`.trim());
338
+ continue;
339
+ }
340
+
341
+ fieldSearchValue = (fieldSearchValue === undefined || fieldSearchValue === null) ? '' : `${fieldSearchValue}`; // make sure it's a string
342
+
343
+ // run regex to find possible filter operators unless the user disabled the feature
344
+ const autoParseInputFilterOperator = columnDef.autoParseInputFilterOperator ?? this._gridOptions.autoParseInputFilterOperator;
345
+ const matches = autoParseInputFilterOperator !== false
346
+ ? fieldSearchValue.match(/^([<>!=\*]{0,2})(.*[^<>!=\*])([\*]?)$/) // group 1: Operator, 2: searchValue, 3: last char is '*' (meaning starts with, ex.: abc*)
347
+ : [fieldSearchValue, '', fieldSearchValue, '']; // when parsing is disabled, we'll only keep the search value in the index 2 to make it easy for code reuse
348
+
349
+ let operator = columnFilter.operator || matches?.[1];
350
+ let searchValue = matches?.[2] || '';
351
+ const lastValueChar = matches?.[3] || (operator === '*z' || operator === OperatorType.endsWith) ? '*' : '';
352
+ const bypassOdataQuery = columnFilter.bypassBackendQuery || false;
353
+
354
+ // no need to query if search value is empty
355
+ if (fieldName && searchValue === '' && searchTerms.length <= 1) {
356
+ this.removeColumnFilter(fieldName);
357
+ continue;
358
+ }
359
+
360
+ if (Array.isArray(searchTerms) && searchTerms.length === 1 && typeof searchTerms[0] === 'string' && searchTerms[0].indexOf('..') >= 0) {
361
+ if (operator !== OperatorType.rangeInclusive && operator !== OperatorType.rangeExclusive) {
362
+ operator = this._gridOptions.defaultFilterRangeOperator ?? OperatorType.rangeInclusive;
363
+ }
364
+
365
+ searchTerms = searchTerms[0].split('..', 2);
366
+ if (searchTerms[0] === '') {
367
+ operator = operator === OperatorType.rangeInclusive ? '<=' : operator === OperatorType.rangeExclusive ? '<' : operator;
368
+ searchTerms = searchTerms.slice(1);
369
+ searchValue = searchTerms[0];
370
+ } else if (searchTerms[1] === '') {
371
+ operator = operator === OperatorType.rangeInclusive ? '>=' : operator === OperatorType.rangeExclusive ? '>' : operator;
372
+ searchTerms = searchTerms.slice(0, 1);
373
+ searchValue = searchTerms[0];
374
+ }
375
+ }
376
+
377
+ // if we didn't find an Operator but we have a Column Operator inside the Filter (DOM Element), we should use its default Operator
378
+ // multipleSelect is "IN", while singleSelect is "EQ", else don't map any operator
379
+ if (!operator && columnDef.filter) {
380
+ operator = columnDef.filter.operator;
381
+ }
382
+
383
+ // No operator and 2 search terms should lead to default range operator.
384
+ if (!operator && Array.isArray(searchTerms) && searchTerms.length === 2 && searchTerms[0] && searchTerms[1]) {
385
+ operator = this._gridOptions.defaultFilterRangeOperator;
386
+ }
387
+
388
+ // Range with 1 searchterm should lead to equals for a date field.
389
+ if ((operator === OperatorType.rangeInclusive || OperatorType.rangeExclusive) && Array.isArray(searchTerms) && searchTerms.length === 1 && fieldType === FieldType.date) {
390
+ operator = OperatorType.equal;
391
+ }
392
+
393
+ // if we still don't have an operator find the proper Operator to use by it's field type
394
+ if (!operator) {
395
+ operator = mapOperatorByFieldType(fieldType);
396
+ }
397
+
398
+ // extra query arguments
399
+ if (bypassOdataQuery) {
400
+ // push to our temp array and also trim white spaces
401
+ if (fieldName) {
402
+ this.saveColumnFilter(fieldName, fieldSearchValue, searchTerms);
403
+ }
404
+ } else {
405
+ // Normalize all search values
406
+ searchValue = this.normalizeSearchValue(fieldType, searchValue, odataVersion);
407
+ if (Array.isArray(searchTerms)) {
408
+ searchTerms.forEach((_part, index) => {
409
+ searchTerms[index] = this.normalizeSearchValue(fieldType, searchTerms[index], odataVersion);
410
+ });
411
+ }
412
+
413
+ searchBy = '';
414
+
415
+ // titleCase the fieldName so that it matches the WebApi names
416
+ if (this._odataService.options.caseType === CaseType.pascalCase) {
417
+ fieldName = titleCase(fieldName || '');
418
+ }
419
+
420
+ if (searchTerms && searchTerms.length > 1 && (operator === 'IN' || operator === 'NIN' || operator === 'NOTIN' || operator === 'NOT IN' || operator === 'NOT_IN')) {
421
+ // when having more than 1 search term (then check if we have a "IN" or "NOT IN" filter search)
422
+ const tmpSearchTerms: string[] = [];
423
+ if (operator === 'IN') {
424
+ // example:: (Stage eq "Expired" or Stage eq "Renewal")
425
+ for (let j = 0, lnj = searchTerms.length; j < lnj; j++) {
426
+ tmpSearchTerms.push(`${fieldName} eq ${searchTerms[j]}`);
427
+ }
428
+ searchBy = tmpSearchTerms.join(' or ');
429
+ } else {
430
+ // example:: (Stage ne "Expired" and Stage ne "Renewal")
431
+ for (let k = 0, lnk = searchTerms.length; k < lnk; k++) {
432
+ tmpSearchTerms.push(`${fieldName} ne ${searchTerms[k]}`);
433
+ }
434
+ searchBy = tmpSearchTerms.join(' and ');
435
+ }
436
+ if (!(typeof searchBy === 'string' && searchBy[0] === '(' && searchBy.slice(-1) === ')')) {
437
+ searchBy = `(${searchBy})`;
438
+ }
439
+ } else if (operator === '*' || operator === 'a*' || operator === '*z' || lastValueChar === '*' || operator === OperatorType.startsWith || operator === OperatorType.endsWith) {
440
+ // first/last character is a '*' will be a startsWith or endsWith
441
+ searchBy = (operator === '*' || operator === '*z' || operator === OperatorType.endsWith) ? `endswith(${fieldName}, ${searchValue})` : `startswith(${fieldName}, ${searchValue})`;
442
+ } else if (operator === OperatorType.rangeExclusive || operator === OperatorType.rangeInclusive) {
443
+ // example:: (Name >= 'Bob' and Name <= 'Jane')
444
+ searchBy = this.filterBySearchTermRange(fieldName, operator, searchTerms);
445
+ } else if ((operator === '' || operator === OperatorType.contains || operator === OperatorType.notContains) &&
446
+ (fieldType === FieldType.string || fieldType === FieldType.text || fieldType === FieldType.readonly)) {
447
+ searchBy = odataVersion >= 4 ? `contains(${fieldName}, ${searchValue})` : `substringof(${searchValue}, ${fieldName})`;
448
+ if (operator === OperatorType.notContains) {
449
+ searchBy = `not ${searchBy}`;
450
+ }
451
+ } else {
452
+ // any other field type (or undefined type)
453
+ searchBy = `${fieldName} ${this.mapOdataOperator(operator)} ${searchValue}`;
454
+ }
455
+
456
+ // push to our temp array and also trim white spaces
457
+ if (searchBy !== '') {
458
+ searchByArray.push(searchBy.trim());
459
+ this.saveColumnFilter(fieldName || '', fieldSearchValue, searchValue);
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ // update the service options with filters for the buildQuery() to work later
466
+ this._odataService.updateOptions({
467
+ filter: (searchByArray.length > 0) ? searchByArray.join(' and ') : '',
468
+ skip: undefined
469
+ });
470
+ }
471
+
472
+ /**
473
+ * Update the pagination component with it's new page number and size
474
+ * @param newPage
475
+ * @param pageSize
476
+ */
477
+ updatePagination(newPage: number, pageSize: number) {
478
+ this._currentPagination = {
479
+ pageNumber: newPage,
480
+ pageSize,
481
+ };
482
+
483
+ // unless user specifically set "enablePagination" to False, we'll update pagination options in every other cases
484
+ if (this._gridOptions && (this._gridOptions.enablePagination || !this._gridOptions.hasOwnProperty('enablePagination'))) {
485
+ this._odataService.updateOptions({
486
+ top: pageSize,
487
+ skip: (newPage - 1) * pageSize
488
+ });
489
+ }
490
+ }
491
+
492
+ /**
493
+ * loop through all columns to inspect sorters & update backend service orderBy
494
+ * @param columnFilters
495
+ */
496
+ updateSorters(sortColumns?: ColumnSort[], presetSorters?: CurrentSorter[]) {
497
+ let currentSorters: CurrentSorter[] = [];
498
+ const odataSorters: OdataSortingOption[] = [];
499
+
500
+ if (!sortColumns && presetSorters) {
501
+ // make the presets the current sorters, also make sure that all direction are in lowercase for OData
502
+ currentSorters = presetSorters;
503
+ currentSorters.forEach((sorter) => sorter.direction = sorter.direction.toLowerCase() as SortDirectionString);
504
+
505
+ // display the correct sorting icons on the UI, for that it requires (columnId, sortAsc) properties
506
+ const tmpSorterArray = currentSorters.map((sorter) => {
507
+ const columnDef = this._columnDefinitions.find((column: Column) => column.id === sorter.columnId);
508
+
509
+ odataSorters.push({
510
+ field: columnDef ? ((columnDef.queryFieldSorter || columnDef.queryField || columnDef.field) + '') : (sorter.columnId + ''),
511
+ direction: sorter.direction
512
+ });
513
+
514
+ // return only the column(s) found in the Column Definitions ELSE null
515
+ if (columnDef) {
516
+ return {
517
+ columnId: sorter.columnId,
518
+ sortAsc: sorter.direction.toUpperCase() === SortDirection.ASC
519
+ };
520
+ }
521
+ return null;
522
+ }) as { columnId: string | number; sortAsc: boolean; }[] | null;
523
+
524
+ // set the sort icons, but also make sure to filter out null values (that happens when columnDef is not found)
525
+ if (Array.isArray(tmpSorterArray) && this._grid) {
526
+ this._grid.setSortColumns(tmpSorterArray);
527
+ }
528
+ } else if (sortColumns && !presetSorters) {
529
+ // build the SortBy string, it could be multisort, example: customerNo asc, purchaserName desc
530
+ if (sortColumns?.length === 0) {
531
+ // TODO fix this line
532
+ // currentSorters = new Array(this.defaultOptions.orderBy); // when empty, use the default sort
533
+ } else {
534
+ if (sortColumns) {
535
+ for (const columnDef of sortColumns) {
536
+ if (columnDef.sortCol) {
537
+ let fieldName = (columnDef.sortCol.queryFieldSorter || columnDef.sortCol.queryField || columnDef.sortCol.field) + '';
538
+ let columnFieldName = (columnDef.sortCol.field || columnDef.sortCol.id) + '';
539
+ let queryField = (columnDef.sortCol.queryFieldSorter || columnDef.sortCol.queryField || columnDef.sortCol.field || '') + '';
540
+ if (this._odataService.options.caseType === CaseType.pascalCase) {
541
+ fieldName = titleCase(fieldName);
542
+ columnFieldName = titleCase(columnFieldName);
543
+ queryField = titleCase(queryField);
544
+ }
545
+
546
+ if (columnFieldName !== '') {
547
+ currentSorters.push({
548
+ columnId: columnFieldName,
549
+ direction: columnDef.sortAsc ? 'asc' : 'desc'
550
+ });
551
+ }
552
+
553
+ if (queryField !== '') {
554
+ odataSorters.push({
555
+ field: queryField,
556
+ direction: columnDef.sortAsc ? SortDirection.ASC : SortDirection.DESC
557
+ });
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ // transform the sortby array into a CSV string for OData
566
+ currentSorters = currentSorters || [] as CurrentSorter[];
567
+ const csvString = odataSorters.map((sorter) => {
568
+ let str = '';
569
+ if (sorter && sorter.field) {
570
+ const sortField = (this._odataService.options.caseType === CaseType.pascalCase) ? titleCase(sorter.field) : sorter.field;
571
+ str = `${sortField} ${sorter && sorter.direction && sorter.direction.toLowerCase() || ''}`;
572
+ }
573
+ return str;
574
+ }).join(',');
575
+
576
+ this._odataService.updateOptions({
577
+ orderBy: csvString
578
+ });
579
+
580
+ // keep current Sorters and update the service options with the new sorting
581
+ this._currentSorters = currentSorters;
582
+
583
+ // build the OData query which we will use in the WebAPI callback
584
+ return this._odataService.buildQuery();
585
+ }
586
+
587
+ //
588
+ // protected functions
589
+ // -------------------
590
+ /**
591
+ * Cast provided filters (could be in multiple format) into an array of ColumnFilter
592
+ * @param columnFilters
593
+ */
594
+ protected castFilterToColumnFilters(columnFilters: ColumnFilters | CurrentFilter[]): CurrentFilter[] {
595
+ // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter)
596
+ const filtersArray: ColumnFilter[] = (typeof columnFilters === 'object') ? Object.keys(columnFilters).map(key => (columnFilters as any)[key]) : columnFilters;
597
+
598
+ if (!Array.isArray(filtersArray)) {
599
+ return [];
600
+ }
601
+
602
+ return filtersArray.map((filter) => {
603
+ const tmpFilter: CurrentFilter = { columnId: filter.columnId || '' };
604
+ if (filter.operator) {
605
+ tmpFilter.operator = filter.operator;
606
+ }
607
+ if (filter.targetSelector) {
608
+ tmpFilter.targetSelector = filter.targetSelector;
609
+ }
610
+ if (Array.isArray(filter.searchTerms)) {
611
+ tmpFilter.searchTerms = filter.searchTerms;
612
+ }
613
+ return tmpFilter;
614
+ });
615
+ }
616
+
617
+ /**
618
+ * Filter by a range of searchTerms (2 searchTerms OR 1 string separated by 2 dots "value1..value2")
619
+ */
620
+ protected filterBySearchTermRange(fieldName: string, operator: OperatorType | OperatorString, searchTerms: SearchTerm[]) {
621
+ let query = '';
622
+ if (Array.isArray(searchTerms) && searchTerms.length === 2) {
623
+ if (operator === OperatorType.rangeInclusive) {
624
+ // example:: (Duration >= 5 and Duration <= 10)
625
+ query = `(${fieldName} ge ${searchTerms[0]}`;
626
+ if (searchTerms[1] !== '') {
627
+ query += ` and ${fieldName} le ${searchTerms[1]}`;
628
+ }
629
+ query += ')';
630
+ } else if (operator === OperatorType.rangeExclusive) {
631
+ // example:: (Duration > 5 and Duration < 10)
632
+ query = `(${fieldName} gt ${searchTerms[0]}`;
633
+ if (searchTerms[1] !== '') {
634
+ query += ` and ${fieldName} lt ${searchTerms[1]}`;
635
+ }
636
+ query += ')';
637
+ }
638
+ }
639
+ return query;
640
+ }
641
+
642
+ /**
643
+ * Normalizes the search value according to field type and oData version.
644
+ */
645
+ protected normalizeSearchValue(fieldType: typeof FieldType[keyof typeof FieldType], searchValue: any, version: number) {
646
+ switch (fieldType) {
647
+ case FieldType.date:
648
+ searchValue = parseUtcDate(searchValue as string, true);
649
+ searchValue = version >= 4 ? searchValue : `DateTime'${searchValue}'`;
650
+ break;
651
+ case FieldType.string:
652
+ case FieldType.text:
653
+ case FieldType.readonly:
654
+ if (typeof searchValue === 'string') {
655
+ // escape single quotes by doubling them
656
+ searchValue = searchValue.replace(/'/g, `''`);
657
+ // encode URI of the final search value
658
+ searchValue = encodeURIComponent(searchValue);
659
+ // strings need to be quoted.
660
+ searchValue = `'${searchValue}'`;
661
+ }
662
+ break;
663
+ case FieldType.integer:
664
+ case FieldType.number:
665
+ case FieldType.float:
666
+ if (typeof searchValue === 'string') {
667
+ // Parse a valid decimal from the string.
668
+
669
+ // Replace double dots with single dots
670
+ searchValue = searchValue.replace(/\.\./g, '.');
671
+ // Remove a trailing dot
672
+ searchValue = searchValue.replace(/\.+$/g, '');
673
+ // Prefix a leading dot with 0
674
+ searchValue = searchValue.replace(/^\.+/g, '0.');
675
+ // Prefix leading dash dot with -0.
676
+ searchValue = searchValue.replace(/^\-+\.+/g, '-0.');
677
+ // Remove any non valid decimal characters from the search string
678
+ searchValue = searchValue.replace(/(?!^\-)[^\d\.]/g, '');
679
+
680
+ // if nothing left, search for 0
681
+ if (searchValue === '' || searchValue === '-') {
682
+ searchValue = '0';
683
+ }
684
+ }
685
+ break;
686
+ }
687
+
688
+ return searchValue;
689
+ }
690
+ }
@@ -0,0 +1,2 @@
1
+ export * from './grid-odata.service';
2
+ export * from './odataQueryBuilder.service';