@budibase/shared-core 2.3.21-alpha.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,32 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
+ };
21
+ var __importStar = (this && this.__importStar) || function (mod) {
22
+ if (mod && mod.__esModule) return mod;
23
+ var result = {};
24
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
25
+ __setModuleDefault(result, mod);
26
+ return result;
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.dataFilters = void 0;
30
+ __exportStar(require("./constants"), exports);
31
+ exports.dataFilters = __importStar(require("./filters"));
32
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@budibase/shared-core",
3
+ "version": "2.3.21-alpha.1",
4
+ "description": "Shared data utils",
5
+ "main": "dist/src/index.js",
6
+ "types": "dist/src/index.d.ts",
7
+ "author": "Budibase",
8
+ "license": "GPL-3.0",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev:builder": "tsc --watch"
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "4.7.3"
15
+ }
16
+ }
@@ -0,0 +1,69 @@
1
+ export const OperatorOptions = {
2
+ Equals: {
3
+ value: "equal",
4
+ label: "Equals",
5
+ },
6
+ NotEquals: {
7
+ value: "notEqual",
8
+ label: "Not equals",
9
+ },
10
+ Empty: {
11
+ value: "empty",
12
+ label: "Is empty",
13
+ },
14
+ NotEmpty: {
15
+ value: "notEmpty",
16
+ label: "Is not empty",
17
+ },
18
+ StartsWith: {
19
+ value: "string",
20
+ label: "Starts with",
21
+ },
22
+ Like: {
23
+ value: "fuzzy",
24
+ label: "Like",
25
+ },
26
+ MoreThan: {
27
+ value: "rangeLow",
28
+ label: "More than or equal to",
29
+ },
30
+ LessThan: {
31
+ value: "rangeHigh",
32
+ label: "Less than or equal to",
33
+ },
34
+ Contains: {
35
+ value: "contains",
36
+ label: "Contains",
37
+ },
38
+ NotContains: {
39
+ value: "notContains",
40
+ label: "Does not contain",
41
+ },
42
+ In: {
43
+ value: "oneOf",
44
+ label: "Is in",
45
+ },
46
+ ContainsAny: {
47
+ value: "containsAny",
48
+ label: "Has any",
49
+ },
50
+ }
51
+
52
+ export const SqlNumberTypeRangeMap = {
53
+ integer: {
54
+ max: 2147483647,
55
+ min: -2147483648,
56
+ },
57
+ int: {
58
+ max: 2147483647,
59
+ min: -2147483648,
60
+ },
61
+ smallint: {
62
+ max: 32767,
63
+ min: -32768,
64
+ },
65
+ mediumint: {
66
+ max: 8388607,
67
+ min: -8388608,
68
+ },
69
+ }
package/src/filters.ts ADDED
@@ -0,0 +1,435 @@
1
+ import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
2
+
3
+ const HBS_REGEX = /{{([^{].*?)}}/g
4
+
5
+ /**
6
+ * Returns the valid operator options for a certain data type
7
+ * @param type the data type
8
+ */
9
+ export const getValidOperatorsForType = (
10
+ type: string,
11
+ field: string,
12
+ datasource: { tableId: string | string[]; type: string }
13
+ ) => {
14
+ const Op = OperatorOptions
15
+ const stringOps = [
16
+ Op.Equals,
17
+ Op.NotEquals,
18
+ Op.StartsWith,
19
+ Op.Like,
20
+ Op.Empty,
21
+ Op.NotEmpty,
22
+ Op.In,
23
+ ]
24
+ const numOps = [
25
+ Op.Equals,
26
+ Op.NotEquals,
27
+ Op.MoreThan,
28
+ Op.LessThan,
29
+ Op.Empty,
30
+ Op.NotEmpty,
31
+ Op.In,
32
+ ]
33
+ let ops: any[] = []
34
+ if (type === "string") {
35
+ ops = stringOps
36
+ } else if (type === "number") {
37
+ ops = numOps
38
+ } else if (type === "options") {
39
+ ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
40
+ } else if (type === "array") {
41
+ ops = [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty, Op.ContainsAny]
42
+ } else if (type === "boolean") {
43
+ ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
44
+ } else if (type === "longform") {
45
+ ops = stringOps
46
+ } else if (type === "datetime") {
47
+ ops = numOps
48
+ } else if (type === "formula") {
49
+ ops = stringOps.concat([Op.MoreThan, Op.LessThan])
50
+ }
51
+
52
+ // Filter out "like" for internal tables
53
+ const externalTable = datasource?.tableId?.includes("datasource_plus")
54
+ if (datasource?.type === "table" && !externalTable) {
55
+ ops = ops.filter(x => x !== Op.Like)
56
+ }
57
+
58
+ // Only allow equal/not equal for _id in SQL tables
59
+ if (field === "_id" && externalTable) {
60
+ ops = [Op.Equals, Op.NotEquals]
61
+ }
62
+
63
+ return ops
64
+ }
65
+
66
+ /**
67
+ * Operators which do not support empty strings as values
68
+ */
69
+ export const NoEmptyFilterStrings = [
70
+ OperatorOptions.StartsWith.value,
71
+ OperatorOptions.Like.value,
72
+ OperatorOptions.Equals.value,
73
+ OperatorOptions.NotEquals.value,
74
+ OperatorOptions.Contains.value,
75
+ OperatorOptions.NotContains.value,
76
+ ]
77
+
78
+ /**
79
+ * Removes any fields that contain empty strings that would cause inconsistent
80
+ * behaviour with how backend tables are filtered (no value means no filter).
81
+ */
82
+ const cleanupQuery = (query: { [x: string]: { [x: string]: any } }) => {
83
+ if (!query) {
84
+ return query
85
+ }
86
+ for (let filterField of NoEmptyFilterStrings) {
87
+ if (!query[filterField]) {
88
+ continue
89
+ }
90
+ for (let [key, value] of Object.entries(query[filterField])) {
91
+ if (value == null || value === "") {
92
+ delete query[filterField][key]
93
+ }
94
+ }
95
+ }
96
+ return query
97
+ }
98
+
99
+ /**
100
+ * Removes a numeric prefix on field names designed to give fields uniqueness
101
+ */
102
+ const removeKeyNumbering = (key: string) => {
103
+ if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
104
+ const parts = key.split(":")
105
+ parts.shift()
106
+ return parts.join(":")
107
+ } else {
108
+ return key
109
+ }
110
+ }
111
+
112
+ type Filter = {
113
+ operator: keyof Query
114
+ field: string
115
+ type: any
116
+ value: any
117
+ externalType: keyof typeof SqlNumberTypeRangeMap
118
+ }
119
+
120
+ type Query = {
121
+ string: Record<string, any>
122
+ fuzzy: Record<string, any>
123
+ range: Record<string, { low: string | number; high: string | number }>
124
+ equal: Record<string, true>
125
+ notEqual: Record<string, true>
126
+ empty: Record<string, any>
127
+ notEmpty: Record<string, any>
128
+ contains: Record<string, any>
129
+ notContains: Record<string, any>
130
+ oneOf: Record<string, any>
131
+ containsAny: Record<string, any>
132
+ allOr?: boolean
133
+ }
134
+
135
+ /**
136
+ * Builds a lucene JSON query from the filter structure generated in the builder
137
+ * @param filter the builder filter structure
138
+ */
139
+ export const buildLuceneQuery = (filter: Filter[]) => {
140
+ let query: Query = {
141
+ string: {},
142
+ fuzzy: {},
143
+ range: {},
144
+ equal: {},
145
+ notEqual: {},
146
+ empty: {},
147
+ notEmpty: {},
148
+ contains: {},
149
+ notContains: {},
150
+ oneOf: {},
151
+ containsAny: {},
152
+ }
153
+ if (Array.isArray(filter)) {
154
+ filter.forEach(expression => {
155
+ let { operator, field, type, value, externalType } = expression
156
+ const isHbs =
157
+ typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
158
+ // Parse all values into correct types
159
+ if (operator === "allOr") {
160
+ query.allOr = true
161
+ return
162
+ }
163
+ if (
164
+ type === "datetime" &&
165
+ !isHbs &&
166
+ operator !== "empty" &&
167
+ operator !== "notEmpty"
168
+ ) {
169
+ // Ensure date value is a valid date and parse into correct format
170
+ if (!value) {
171
+ return
172
+ }
173
+ try {
174
+ value = new Date(value).toISOString()
175
+ } catch (error) {
176
+ return
177
+ }
178
+ }
179
+ if (type === "number" && typeof value === "string") {
180
+ if (operator === "oneOf") {
181
+ value = value.split(",").map(item => parseFloat(item))
182
+ } else if (!isHbs) {
183
+ value = parseFloat(value)
184
+ }
185
+ }
186
+ if (type === "boolean") {
187
+ value = `${value}`?.toLowerCase() === "true"
188
+ }
189
+ if (
190
+ ["contains", "notContains", "containsAny"].includes(operator) &&
191
+ type === "array" &&
192
+ typeof value === "string"
193
+ ) {
194
+ value = value.split(",")
195
+ }
196
+ if (operator.startsWith("range")) {
197
+ const minint =
198
+ SqlNumberTypeRangeMap[externalType]?.min || Number.MIN_SAFE_INTEGER
199
+ const maxint =
200
+ SqlNumberTypeRangeMap[externalType]?.max || Number.MAX_SAFE_INTEGER
201
+ if (!query.range[field]) {
202
+ query.range[field] = {
203
+ low: type === "number" ? minint : "0000-00-00T00:00:00.000Z",
204
+ high: type === "number" ? maxint : "9999-00-00T00:00:00.000Z",
205
+ }
206
+ }
207
+ if ((operator as any) === "rangeLow" && value != null && value !== "") {
208
+ query.range[field].low = value
209
+ } else if (
210
+ (operator as any) === "rangeHigh" &&
211
+ value != null &&
212
+ value !== ""
213
+ ) {
214
+ query.range[field].high = value
215
+ }
216
+ } else if (query[operator]) {
217
+ if (type === "boolean") {
218
+ // Transform boolean filters to cope with null.
219
+ // "equals false" needs to be "not equals true"
220
+ // "not equals false" needs to be "equals true"
221
+ if (operator === "equal" && value === false) {
222
+ query.notEqual[field] = true
223
+ } else if (operator === "notEqual" && value === false) {
224
+ query.equal[field] = true
225
+ } else {
226
+ query[operator][field] = value
227
+ }
228
+ } else {
229
+ query[operator][field] = value
230
+ }
231
+ }
232
+ })
233
+ }
234
+ return query
235
+ }
236
+
237
+ const deepGet = (obj: { [x: string]: any }, key: string) => {
238
+ if (!obj || !key) {
239
+ return null
240
+ }
241
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
242
+ return obj[key]
243
+ }
244
+ const split = key.split(".")
245
+ for (let i = 0; i < split.length; i++) {
246
+ obj = obj?.[split[i]]
247
+ }
248
+ return obj
249
+ }
250
+
251
+ /**
252
+ * Performs a client-side lucene search on an array of data
253
+ * @param docs the data
254
+ * @param query the JSON lucene query
255
+ */
256
+ export const runLuceneQuery = (
257
+ docs: any[],
258
+ query?: { [x: string]: any; sheet?: string }
259
+ ) => {
260
+ if (!docs || !Array.isArray(docs)) {
261
+ return []
262
+ }
263
+ if (!query) {
264
+ return docs
265
+ }
266
+
267
+ // Make query consistent first
268
+ query = cleanupQuery(query)
269
+
270
+ // Iterates over a set of filters and evaluates a fail function against a doc
271
+ const match =
272
+ (type: string, failFn: (docValue: any, testValue: any) => boolean) =>
273
+ (doc: any) => {
274
+ const filters = Object.entries(query![type] || {})
275
+ for (let i = 0; i < filters.length; i++) {
276
+ const [key, testValue] = filters[i]
277
+ const docValue = deepGet(doc, removeKeyNumbering(key))
278
+ if (failFn(docValue, testValue)) {
279
+ return false
280
+ }
281
+ }
282
+ return true
283
+ }
284
+
285
+ // Process a string match (fails if the value does not start with the string)
286
+ const stringMatch = match("string", (docValue: string, testValue: string) => {
287
+ return (
288
+ !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
289
+ )
290
+ })
291
+
292
+ // Process a fuzzy match (treat the same as starts with when running locally)
293
+ const fuzzyMatch = match("fuzzy", (docValue: string, testValue: string) => {
294
+ return (
295
+ !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
296
+ )
297
+ })
298
+
299
+ // Process a range match
300
+ const rangeMatch = match(
301
+ "range",
302
+ (
303
+ docValue: string | number | null,
304
+ testValue: { low: number; high: number }
305
+ ) => {
306
+ return (
307
+ docValue == null ||
308
+ docValue === "" ||
309
+ docValue < testValue.low ||
310
+ docValue > testValue.high
311
+ )
312
+ }
313
+ )
314
+
315
+ // Process an equal match (fails if the value is different)
316
+ const equalMatch = match(
317
+ "equal",
318
+ (docValue: any, testValue: string | null) => {
319
+ return testValue != null && testValue !== "" && docValue !== testValue
320
+ }
321
+ )
322
+
323
+ // Process a not-equal match (fails if the value is the same)
324
+ const notEqualMatch = match(
325
+ "notEqual",
326
+ (docValue: any, testValue: string | null) => {
327
+ return testValue != null && testValue !== "" && docValue === testValue
328
+ }
329
+ )
330
+
331
+ // Process an empty match (fails if the value is not empty)
332
+ const emptyMatch = match("empty", (docValue: string | null) => {
333
+ return docValue != null && docValue !== ""
334
+ })
335
+
336
+ // Process a not-empty match (fails is the value is empty)
337
+ const notEmptyMatch = match("notEmpty", (docValue: string | null) => {
338
+ return docValue == null || docValue === ""
339
+ })
340
+
341
+ // Process an includes match (fails if the value is not included)
342
+ const oneOf = match("oneOf", (docValue: any, testValue: any) => {
343
+ if (typeof testValue === "string") {
344
+ testValue = testValue.split(",")
345
+ if (typeof docValue === "number") {
346
+ testValue = testValue.map((item: string) => parseFloat(item))
347
+ }
348
+ }
349
+ return !testValue?.includes(docValue)
350
+ })
351
+
352
+ const containsAny = match("containsAny", (docValue: any, testValue: any) => {
353
+ return !docValue?.includes(...testValue)
354
+ })
355
+
356
+ const contains = match(
357
+ "contains",
358
+ (docValue: string | any[], testValue: any[]) => {
359
+ return !testValue?.every((item: any) => docValue?.includes(item))
360
+ }
361
+ )
362
+
363
+ const notContains = match(
364
+ "notContains",
365
+ (docValue: string | any[], testValue: any[]) => {
366
+ return testValue?.every((item: any) => docValue?.includes(item))
367
+ }
368
+ )
369
+
370
+ // Match a document against all criteria
371
+ const docMatch = (doc: any) => {
372
+ return (
373
+ stringMatch(doc) &&
374
+ fuzzyMatch(doc) &&
375
+ rangeMatch(doc) &&
376
+ equalMatch(doc) &&
377
+ notEqualMatch(doc) &&
378
+ emptyMatch(doc) &&
379
+ notEmptyMatch(doc) &&
380
+ oneOf(doc) &&
381
+ contains(doc) &&
382
+ containsAny(doc) &&
383
+ notContains(doc)
384
+ )
385
+ }
386
+
387
+ // Process all docs
388
+ return docs.filter(docMatch)
389
+ }
390
+
391
+ /**
392
+ * Performs a client-side sort from the equivalent server-side lucene sort
393
+ * parameters.
394
+ * @param docs the data
395
+ * @param sort the sort column
396
+ * @param sortOrder the sort order ("ascending" or "descending")
397
+ * @param sortType the type of sort ("string" or "number")
398
+ */
399
+ export const luceneSort = (
400
+ docs: any[],
401
+ sort: string | number,
402
+ sortOrder: string,
403
+ sortType = "string"
404
+ ) => {
405
+ if (!sort || !sortOrder || !sortType) {
406
+ return docs
407
+ }
408
+ const parse =
409
+ sortType === "string" ? (x: any) => `${x}` : (x: string) => parseFloat(x)
410
+ return docs
411
+ .slice()
412
+ .sort((a: { [x: string]: any }, b: { [x: string]: any }) => {
413
+ const colA = parse(a[sort])
414
+ const colB = parse(b[sort])
415
+ if (sortOrder === "Descending") {
416
+ return colA > colB ? -1 : 1
417
+ } else {
418
+ return colA > colB ? 1 : -1
419
+ }
420
+ })
421
+ }
422
+
423
+ /**
424
+ * Limits the specified docs to the specified number of rows from the equivalent
425
+ * server-side lucene limit parameters.
426
+ * @param docs the data
427
+ * @param limit the number of docs to limit to
428
+ */
429
+ export const luceneLimit = (docs: string | any[], limit: string) => {
430
+ const numLimit = parseFloat(limit)
431
+ if (isNaN(numLimit)) {
432
+ return docs
433
+ }
434
+ return docs.slice(0, numLimit)
435
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./constants"
2
+ export * as dataFilters from "./filters"
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "module": "commonjs",
5
+ "lib": ["es2020"],
6
+ "strict": true,
7
+ "noImplicitAny": true,
8
+ "esModuleInterop": true,
9
+ "resolveJsonModule": true,
10
+ "incremental": true,
11
+ "sourceMap": true,
12
+ "declaration": true,
13
+ "types": ["node"],
14
+ "outDir": "dist",
15
+ "skipLibCheck": true
16
+ },
17
+ "include": ["**/*.js", "**/*.ts", "package.json"],
18
+ "exclude": [
19
+ "node_modules",
20
+ "dist",
21
+ "**/*.spec.ts",
22
+ "**/*.spec.js",
23
+ "__mocks__"
24
+ ]
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "composite": true
5
+ }
6
+ }