tablevu 0.2.22 → 0.3.0-beta.1

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,519 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.tablevu = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /* ==========================================================================
8
+ * OData DataTable Query Builder
9
+ * ========================================================================== */
10
+ /* ==========================================================================
11
+ * Value
12
+ * ========================================================================== */
13
+ function formatValue(value) {
14
+ if (value === null)
15
+ return 'null';
16
+ if (typeof value === 'string')
17
+ return `'${escapeODataString(value)}'`;
18
+ if (typeof value === 'number') {
19
+ if (!Number.isFinite(value))
20
+ throw new Error('Invalid numeric value.');
21
+ return String(value);
22
+ }
23
+ if (typeof value === 'boolean')
24
+ return value ? 'true' : 'false';
25
+ if (value instanceof Date)
26
+ return value.toISOString();
27
+ /*
28
+ * Backward compatibility with:
29
+ *
30
+ * {
31
+ * func: 'now',
32
+ * params: []
33
+ * }
34
+ */
35
+ if (typeof value === 'object' &&
36
+ value !== null &&
37
+ 'func' in value &&
38
+ 'params' in value) {
39
+ const funcValue = value;
40
+ return (funcValue.func +
41
+ '(' +
42
+ funcValue.params
43
+ .map(m => formatValue(m))
44
+ .join(',') +
45
+ ')');
46
+ }
47
+ throw new Error(`Unsupported OData value type: ${typeof value}`);
48
+ }
49
+ function escapeODataString(value) {
50
+ /*
51
+ * OData escapes a single quote by doubling it.
52
+ *
53
+ * O'Reilly
54
+ * ↓
55
+ * 'O''Reilly'
56
+ */
57
+ return value.replace(/'/g, "''");
58
+ }
59
+ /* ==========================================================================
60
+ * Filter
61
+ * ========================================================================== */
62
+ function toODataFilter(filter) {
63
+ return buildFilter(filter);
64
+ }
65
+ function buildFilter(filter) {
66
+ if (!filter)
67
+ throw new Error('Invalid filter.');
68
+ /*
69
+ * Backward compatibility:
70
+ *
71
+ * {
72
+ * func: 'contains',
73
+ * params: [...]
74
+ * }
75
+ */
76
+ if (filter.func) {
77
+ return buildFunctionObject(filter);
78
+ }
79
+ switch (filter.operator) {
80
+ /* ------------------------------------------------------------------ */
81
+ /* Logical */
82
+ /* ------------------------------------------------------------------ */
83
+ case 'and':
84
+ return buildLogical(filter, 'and');
85
+ case 'or':
86
+ return buildLogical(filter, 'or');
87
+ case 'not':
88
+ return buildNot(filter);
89
+ /* ------------------------------------------------------------------ */
90
+ /* Comparison */
91
+ /* ------------------------------------------------------------------ */
92
+ case 'eq':
93
+ case 'ne':
94
+ case 'gt':
95
+ case 'ge':
96
+ case 'lt':
97
+ case 'le':
98
+ return buildComparison(filter);
99
+ /* ------------------------------------------------------------------ */
100
+ /* String */
101
+ /* ------------------------------------------------------------------ */
102
+ case 'contains':
103
+ return buildFunction(filter, 'contains');
104
+ case 'startsWith':
105
+ return buildFunction(filter, 'startswith');
106
+ case 'endsWith':
107
+ return buildFunction(filter, 'endswith');
108
+ /* ------------------------------------------------------------------ */
109
+ /* Value */
110
+ /* ------------------------------------------------------------------ */
111
+ case 'in':
112
+ return buildIn(filter);
113
+ case 'between':
114
+ return buildBetween(filter);
115
+ case 'isNull':
116
+ return `${getColumn(filter)} eq null`;
117
+ case 'isNotNull':
118
+ return `${getColumn(filter)} ne null`;
119
+ /* ------------------------------------------------------------------ */
120
+ /* Collection */
121
+ /* ------------------------------------------------------------------ */
122
+ case 'any':
123
+ return buildAny(filter);
124
+ case 'all':
125
+ return buildAll(filter);
126
+ /* ------------------------------------------------------------------ */
127
+ /* Collection count */
128
+ /* ------------------------------------------------------------------ */
129
+ case 'countGt':
130
+ case 'countGe':
131
+ case 'countLt':
132
+ case 'countLe':
133
+ return buildCount(filter);
134
+ default:
135
+ throw new Error(`Unsupported OData operator: ${filter.operator}`);
136
+ }
137
+ }
138
+ /* ==========================================================================
139
+ * Logical
140
+ * ========================================================================== */
141
+ function buildLogical(filter, operator) {
142
+ const filters = requireFilters(filter);
143
+ if (filters.length === 0)
144
+ throw new Error(`Operator '${filter.operator}' requires at least one filter.`);
145
+ return filters
146
+ .map(buildFilter)
147
+ .map(expression => `(${expression})`)
148
+ .join(` ${operator} `);
149
+ }
150
+ function buildNot(filter) {
151
+ const filters = requireFilters(filter);
152
+ if (filters.length !== 1)
153
+ throw new Error("Operator 'not' requires exactly one filter.");
154
+ return `not (${buildFilter(filters[0])})`;
155
+ }
156
+ /* ==========================================================================
157
+ * Comparison
158
+ * ========================================================================== */
159
+ function buildComparison(filter) {
160
+ const column = getColumn(filter);
161
+ return (`${column} ` +
162
+ `${filter.operator} ` +
163
+ `${formatValue(filter.value)}`);
164
+ }
165
+ /* ==========================================================================
166
+ * String functions
167
+ * ========================================================================== */
168
+ function buildFunction(filter, functionName) {
169
+ const column = getColumn(filter);
170
+ return (`${functionName}(` +
171
+ `${column},` +
172
+ `${formatValue(filter.value)}` +
173
+ `)`);
174
+ }
175
+ /* ==========================================================================
176
+ * Function object
177
+ *
178
+ * Backward compatibility with the old:
179
+ *
180
+ * {
181
+ * func: 'contains',
182
+ * params: [...]
183
+ * }
184
+ * ========================================================================== */
185
+ function buildFunctionObject(filter) {
186
+ if (!filter.func)
187
+ throw new Error('Function is required.');
188
+ return (filter.func +
189
+ '(' +
190
+ (filter.params ?? [])
191
+ .map(m => formatValue(m))
192
+ .join(',') +
193
+ ')');
194
+ }
195
+ /* ==========================================================================
196
+ * IN
197
+ * ========================================================================== */
198
+ function buildIn(filter) {
199
+ const column = getColumn(filter);
200
+ if (!Array.isArray(filter.value))
201
+ throw new Error("Operator 'in' requires an array value.");
202
+ if (filter.value.length === 0)
203
+ throw new Error("Operator 'in' requires at least one value.");
204
+ return (`${column} in (` +
205
+ filter.value
206
+ .map(formatValue)
207
+ .join(',') +
208
+ `)`);
209
+ }
210
+ /* ==========================================================================
211
+ * BETWEEN
212
+ * ========================================================================== */
213
+ function buildBetween(filter) {
214
+ const column = getColumn(filter);
215
+ if (!Array.isArray(filter.value) ||
216
+ filter.value.length !== 2) {
217
+ throw new Error("Operator 'between' requires exactly two values.");
218
+ }
219
+ const [min, max] = filter.value;
220
+ return (`(${column} ge ${formatValue(min)} ` +
221
+ `and ${column} le ${formatValue(max)})`);
222
+ }
223
+ /* ==========================================================================
224
+ * ANY
225
+ * ========================================================================== */
226
+ function buildAny(filter) {
227
+ const column = getColumn(filter);
228
+ /*
229
+ * orders/any()
230
+ */
231
+ if (!filter.filters?.length)
232
+ return `${column}/any()`;
233
+ if (filter.filters.length !== 1)
234
+ throw new Error("Operator 'any' requires exactly one filter.");
235
+ return (`${column}/any(x: ` +
236
+ `${buildLambdaFilter(filter.filters[0], 'x')}` +
237
+ `)`);
238
+ }
239
+ /* ==========================================================================
240
+ * ALL
241
+ * ========================================================================== */
242
+ function buildAll(filter) {
243
+ const column = getColumn(filter);
244
+ const filters = requireFilters(filter);
245
+ if (filters.length !== 1)
246
+ throw new Error("Operator 'all' requires exactly one filter.");
247
+ return (`${column}/all(x: ` +
248
+ `${buildLambdaFilter(filters[0], 'x')}` +
249
+ `)`);
250
+ }
251
+ /* ==========================================================================
252
+ * Lambda filter
253
+ * ========================================================================== */
254
+ function buildLambdaFilter(filter, variable) {
255
+ /*
256
+ * Logical
257
+ */
258
+ if (filter.operator === 'and')
259
+ return buildLambdaLogical(filter, variable, 'and');
260
+ if (filter.operator === 'or')
261
+ return buildLambdaLogical(filter, variable, 'or');
262
+ if (filter.operator === 'not') {
263
+ const filters = requireFilters(filter);
264
+ if (filters.length !== 1)
265
+ throw new Error("Operator 'not' requires exactly one filter.");
266
+ return (`not (` +
267
+ `${buildLambdaFilter(filters[0], variable)}` +
268
+ `)`);
269
+ }
270
+ /*
271
+ * Comparison
272
+ */
273
+ switch (filter.operator) {
274
+ case 'eq':
275
+ case 'ne':
276
+ case 'gt':
277
+ case 'ge':
278
+ case 'lt':
279
+ case 'le':
280
+ return (`${variable}/${getColumn(filter)} ` +
281
+ `${filter.operator} ` +
282
+ `${formatValue(filter.value)}`);
283
+ /*
284
+ * String
285
+ */
286
+ case 'contains':
287
+ return (`contains(` +
288
+ `${getLambdaColumn(filter, variable)},` +
289
+ `${formatValue(filter.value)}` +
290
+ `)`);
291
+ case 'startsWith':
292
+ return (`startswith(` +
293
+ `${getLambdaColumn(filter, variable)},` +
294
+ `${formatValue(filter.value)}` +
295
+ `)`);
296
+ case 'endsWith':
297
+ return (`endswith(` +
298
+ `${getLambdaColumn(filter, variable)},` +
299
+ `${formatValue(filter.value)}` +
300
+ `)`);
301
+ /*
302
+ * Null
303
+ */
304
+ case 'isNull':
305
+ return (`${getLambdaColumn(filter, variable)} eq null`);
306
+ case 'isNotNull':
307
+ return (`${getLambdaColumn(filter, variable)} ne null`);
308
+ default:
309
+ throw new Error(`Operator '${filter.operator}' ` +
310
+ `is not supported inside any/all.`);
311
+ }
312
+ }
313
+ function buildLambdaLogical(filter, variable, operator) {
314
+ const filters = requireFilters(filter);
315
+ if (filters.length === 0)
316
+ throw new Error(`Operator '${filter.operator}' requires at least one filter.`);
317
+ return filters
318
+ .map(item => buildLambdaFilter(item, variable))
319
+ .map(expression => `(${expression})`)
320
+ .join(` ${operator} `);
321
+ }
322
+ /* ==========================================================================
323
+ * Collection count
324
+ * ========================================================================== */
325
+ function buildCount(filter) {
326
+ const column = getColumn(filter);
327
+ const operators = {
328
+ countGt: 'gt',
329
+ countGe: 'ge',
330
+ countLt: 'lt',
331
+ countLe: 'le'
332
+ };
333
+ return (`${column}/$count ` +
334
+ `${operators[filter.operator]} ` +
335
+ `${formatValue(filter.value)}`);
336
+ }
337
+ /* ==========================================================================
338
+ * Helpers
339
+ * ========================================================================== */
340
+ function getColumn(filter) {
341
+ if (!filter.column ||
342
+ filter.column.trim().length === 0) {
343
+ throw new Error(`Operator '${filter.operator}' requires a column.`);
344
+ }
345
+ /*
346
+ * Allow:
347
+ *
348
+ * customer.name
349
+ * customer.address.city
350
+ *
351
+ * to become:
352
+ *
353
+ * customer/name
354
+ * customer/address/city
355
+ */
356
+ return filter.column.replace(/\./g, '/');
357
+ }
358
+ function getLambdaColumn(filter, variable) {
359
+ return (`${variable}/` +
360
+ `${getColumn(filter)}`);
361
+ }
362
+ function requireFilters(filter) {
363
+ if (!filter.filters)
364
+ throw new Error(`Operator '${filter.operator}' requires filters.`);
365
+ return filter.filters;
366
+ }
367
+ /* ==========================================================================
368
+ * OData Request
369
+ * ========================================================================== */
370
+ function odata(options, props = {}) {
371
+ const url = new URL(options.url);
372
+ /* ---------------------------------------------------------------------- */
373
+ /* Paging */
374
+ /* ---------------------------------------------------------------------- */
375
+ if (props.pageSize &&
376
+ props.pageSize > 0) {
377
+ const currPage = props.currPage && props.currPage > 0
378
+ ? props.currPage
379
+ : 1;
380
+ const skip = props.pageSize * (currPage - 1);
381
+ url.searchParams.set('$skip', String(skip));
382
+ url.searchParams.set('$top', String(props.pageSize));
383
+ }
384
+ /* ---------------------------------------------------------------------- */
385
+ /* Count */
386
+ /* ---------------------------------------------------------------------- */
387
+ if (options.count === true) {
388
+ url.searchParams.set('$count', 'true');
389
+ }
390
+ /* ---------------------------------------------------------------------- */
391
+ /* Select */
392
+ /* ---------------------------------------------------------------------- */
393
+ if (options.select &&
394
+ options.select.length > 0) {
395
+ url.searchParams.set('$select', options.select
396
+ .map(m => m.replace(/\./g, '/'))
397
+ .join(','));
398
+ }
399
+ /* ---------------------------------------------------------------------- */
400
+ /* Expand */
401
+ /* ---------------------------------------------------------------------- */
402
+ if (options.expand &&
403
+ options.expand.length > 0) {
404
+ const expand = options.expand
405
+ .map(expand => {
406
+ const name = expand.name.replace(/\./g, '/');
407
+ if (expand.select &&
408
+ expand.select.length > 0) {
409
+ return (`${name}` +
410
+ `($select=` +
411
+ `${expand.select
412
+ .map(m => m.replace(/\./g, '/'))
413
+ .join(',')}` +
414
+ `)`);
415
+ }
416
+ return name;
417
+ })
418
+ .join(',');
419
+ url.searchParams.set('$expand', expand);
420
+ }
421
+ /* ---------------------------------------------------------------------- */
422
+ /* Filter */
423
+ /* ---------------------------------------------------------------------- */
424
+ const filters = [];
425
+ /*
426
+ * Static filter from options
427
+ */
428
+ if (options.filter &&
429
+ (options.filter.filters ||
430
+ options.filter.column ||
431
+ options.filter.func)) {
432
+ const expression = toODataFilter(options.filter);
433
+ if (expression)
434
+ filters.push(`(${expression})`);
435
+ }
436
+ /*
437
+ * Runtime filter from DataTable
438
+ */
439
+ if (props.filter &&
440
+ (props.filter.filters ||
441
+ props.filter.column ||
442
+ props.filter.func)) {
443
+ const expression = toODataFilter(props.filter);
444
+ if (expression)
445
+ filters.push(`(${expression})`);
446
+ }
447
+ /* ---------------------------------------------------------------------- */
448
+ /* Search */
449
+ /* ---------------------------------------------------------------------- */
450
+ if (props.search &&
451
+ props.search.trim() &&
452
+ props.cols &&
453
+ props.cols.length > 0) {
454
+ const searchValue = escapeODataString(props.search.trim());
455
+ const searchExpressions = props.cols
456
+ .filter(column => column.searchable === undefined ||
457
+ column.searchable === true)
458
+ .map(column => {
459
+ const name = column.name.replace(/\./g, '/');
460
+ return (`contains(` +
461
+ `${name},` +
462
+ `'${searchValue}'` +
463
+ `)`);
464
+ });
465
+ if (searchExpressions.length > 0) {
466
+ filters.push(`(${searchExpressions.join(' or ')})`);
467
+ }
468
+ }
469
+ /*
470
+ * Combine all filters with AND.
471
+ */
472
+ if (filters.length > 0) {
473
+ url.searchParams.set('$filter', filters.join(' and '));
474
+ }
475
+ /* ---------------------------------------------------------------------- */
476
+ /* Order By */
477
+ /* ---------------------------------------------------------------------- */
478
+ if (props.sort &&
479
+ props.sort.length > 0) {
480
+ const orderBy = props.sort
481
+ .map(sort => {
482
+ let column;
483
+ if (sort.index !== undefined &&
484
+ sort.index >= 0) {
485
+ if (!props.cols ||
486
+ !props.cols[sort.index]) {
487
+ throw new Error(`Invalid sort column index: ${sort.index}`);
488
+ }
489
+ column =
490
+ props.cols[sort.index].name;
491
+ }
492
+ else if (sort.name) {
493
+ column = sort.name;
494
+ }
495
+ else {
496
+ throw new Error('Sort requires either index or name.');
497
+ }
498
+ column =
499
+ column.replace(/\./g, '/');
500
+ return (column +
501
+ (sort.dir
502
+ ? ` ${sort.dir}`
503
+ : ''));
504
+ })
505
+ .join(',');
506
+ url.searchParams.set('$orderby', orderBy);
507
+ }
508
+ /* ---------------------------------------------------------------------- */
509
+ /* Request */
510
+ /* ---------------------------------------------------------------------- */
511
+ return new Request(url, options.header);
512
+ }
513
+
514
+ exports.default = odata;
515
+ exports.toODataFilter = toODataFilter;
516
+
517
+ Object.defineProperty(exports, '__esModule', { value: true });
518
+
519
+ }));