@budibase/frontend-core 2.4.12-alpha.0 → 2.4.12

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.
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.4.12-alpha.0",
3
+ "version": "2.4.12",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
7
7
  "svelte": "src/index.js",
8
8
  "dependencies": {
9
- "@budibase/bbui": "2.4.12-alpha.0",
10
- "@budibase/shared-core": "2.4.12-alpha.0",
9
+ "@budibase/bbui": "^2.4.12",
11
10
  "lodash": "^4.17.21",
12
11
  "svelte": "^3.46.2"
13
12
  },
14
- "gitHead": "b5b20e239ee8e5d1172dca2a16538587a58a91e4"
13
+ "gitHead": "ac4c0b3dd60c901691c7644c2f9aa6a5458b03f4"
15
14
  }
package/src/constants.js CHANGED
@@ -1,7 +1,56 @@
1
1
  /**
2
2
  * Operator options for lucene queries
3
3
  */
4
- export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core"
4
+ export const OperatorOptions = {
5
+ Equals: {
6
+ value: "equal",
7
+ label: "Equals",
8
+ },
9
+ NotEquals: {
10
+ value: "notEqual",
11
+ label: "Not equals",
12
+ },
13
+ Empty: {
14
+ value: "empty",
15
+ label: "Is empty",
16
+ },
17
+ NotEmpty: {
18
+ value: "notEmpty",
19
+ label: "Is not empty",
20
+ },
21
+ StartsWith: {
22
+ value: "string",
23
+ label: "Starts with",
24
+ },
25
+ Like: {
26
+ value: "fuzzy",
27
+ label: "Like",
28
+ },
29
+ MoreThan: {
30
+ value: "rangeLow",
31
+ label: "More than or equal to",
32
+ },
33
+ LessThan: {
34
+ value: "rangeHigh",
35
+ label: "Less than or equal to",
36
+ },
37
+ Contains: {
38
+ value: "contains",
39
+ label: "Contains",
40
+ },
41
+ NotContains: {
42
+ value: "notContains",
43
+ label: "Does not contain",
44
+ },
45
+ In: {
46
+ value: "oneOf",
47
+ label: "Is in",
48
+ },
49
+ ContainsAny: {
50
+ value: "containsAny",
51
+ label: "Has any",
52
+ },
53
+ }
5
54
 
6
55
  // Cookie names
7
56
  export const Cookies = {
@@ -78,6 +127,27 @@ export const Roles = {
78
127
  PUBLIC: "PUBLIC",
79
128
  BUILDER: "BUILDER",
80
129
  }
130
+ /**
131
+ * Maximum minimum range for SQL number values
132
+ */
133
+ export const SqlNumberTypeRangeMap = {
134
+ integer: {
135
+ max: 2147483647,
136
+ min: -2147483648,
137
+ },
138
+ int: {
139
+ max: 2147483647,
140
+ min: -2147483648,
141
+ },
142
+ smallint: {
143
+ max: 32767,
144
+ min: -32768,
145
+ },
146
+ mediumint: {
147
+ max: 8388607,
148
+ min: -8388608,
149
+ },
150
+ }
81
151
 
82
152
  export const Themes = [
83
153
  {
@@ -1,11 +1,13 @@
1
1
  import { writable, derived, get } from "svelte/store"
2
2
  import { cloneDeep } from "lodash/fp"
3
- import { LuceneUtils } from "../utils"
3
+ import {
4
+ buildLuceneQuery,
5
+ luceneLimit,
6
+ runLuceneQuery,
7
+ luceneSort,
8
+ } from "../utils/lucene"
4
9
  import { convertJSONSchemaToTableSchema } from "../utils/json"
5
10
 
6
- const { buildLuceneQuery, luceneLimit, runLuceneQuery, luceneSort } =
7
- LuceneUtils
8
-
9
11
  /**
10
12
  * Parent class which handles the implementation of fetching data from an
11
13
  * internal table or datasource plus.
@@ -1,4 +1,4 @@
1
- export { dataFilters as LuceneUtils } from "@budibase/shared-core"
1
+ export * as LuceneUtils from "./lucene"
2
2
  export * as JSONUtils from "./json"
3
3
  export * as CookieUtils from "./cookies"
4
4
  export * as RoleUtils from "./roles"
@@ -0,0 +1,360 @@
1
+ import { Helpers } from "@budibase/bbui"
2
+ import { OperatorOptions, SqlNumberTypeRangeMap } from "../constants"
3
+
4
+ const HBS_REGEX = /{{([^{].*?)}}/g
5
+
6
+ /**
7
+ * Returns the valid operator options for a certain data type
8
+ * @param type the data type
9
+ */
10
+ export const getValidOperatorsForType = (type, field, datasource) => {
11
+ const Op = OperatorOptions
12
+ const stringOps = [
13
+ Op.Equals,
14
+ Op.NotEquals,
15
+ Op.StartsWith,
16
+ Op.Like,
17
+ Op.Empty,
18
+ Op.NotEmpty,
19
+ Op.In,
20
+ ]
21
+ const numOps = [
22
+ Op.Equals,
23
+ Op.NotEquals,
24
+ Op.MoreThan,
25
+ Op.LessThan,
26
+ Op.Empty,
27
+ Op.NotEmpty,
28
+ Op.In,
29
+ ]
30
+ let ops = []
31
+ if (type === "string") {
32
+ ops = stringOps
33
+ } else if (type === "number") {
34
+ ops = numOps
35
+ } else if (type === "options") {
36
+ ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
37
+ } else if (type === "array") {
38
+ ops = [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty, Op.ContainsAny]
39
+ } else if (type === "boolean") {
40
+ ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
41
+ } else if (type === "longform") {
42
+ ops = stringOps
43
+ } else if (type === "datetime") {
44
+ ops = numOps
45
+ } else if (type === "formula") {
46
+ ops = stringOps.concat([Op.MoreThan, Op.LessThan])
47
+ }
48
+
49
+ // Filter out "like" for internal tables
50
+ const externalTable = datasource?.tableId?.includes("datasource_plus")
51
+ if (datasource?.type === "table" && !externalTable) {
52
+ ops = ops.filter(x => x !== Op.Like)
53
+ }
54
+
55
+ // Only allow equal/not equal for _id in SQL tables
56
+ if (field === "_id" && externalTable) {
57
+ ops = [Op.Equals, Op.NotEquals]
58
+ }
59
+
60
+ return ops
61
+ }
62
+
63
+ /**
64
+ * Operators which do not support empty strings as values
65
+ */
66
+ export const NoEmptyFilterStrings = [
67
+ OperatorOptions.StartsWith.value,
68
+ OperatorOptions.Like.value,
69
+ OperatorOptions.Equals.value,
70
+ OperatorOptions.NotEquals.value,
71
+ OperatorOptions.Contains.value,
72
+ OperatorOptions.NotContains.value,
73
+ ]
74
+
75
+ /**
76
+ * Removes any fields that contain empty strings that would cause inconsistent
77
+ * behaviour with how backend tables are filtered (no value means no filter).
78
+ */
79
+ const cleanupQuery = query => {
80
+ if (!query) {
81
+ return query
82
+ }
83
+ for (let filterField of NoEmptyFilterStrings) {
84
+ if (!query[filterField]) {
85
+ continue
86
+ }
87
+ for (let [key, value] of Object.entries(query[filterField])) {
88
+ if (value == null || value === "") {
89
+ delete query[filterField][key]
90
+ }
91
+ }
92
+ }
93
+ return query
94
+ }
95
+
96
+ /**
97
+ * Removes a numeric prefix on field names designed to give fields uniqueness
98
+ */
99
+ const removeKeyNumbering = key => {
100
+ if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
101
+ const parts = key.split(":")
102
+ parts.shift()
103
+ return parts.join(":")
104
+ } else {
105
+ return key
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Builds a lucene JSON query from the filter structure generated in the builder
111
+ * @param filter the builder filter structure
112
+ */
113
+ export const buildLuceneQuery = filter => {
114
+ let query = {
115
+ string: {},
116
+ fuzzy: {},
117
+ range: {},
118
+ equal: {},
119
+ notEqual: {},
120
+ empty: {},
121
+ notEmpty: {},
122
+ contains: {},
123
+ notContains: {},
124
+ oneOf: {},
125
+ containsAny: {},
126
+ }
127
+ if (Array.isArray(filter)) {
128
+ filter.forEach(expression => {
129
+ let { operator, field, type, value, externalType } = expression
130
+ const isHbs =
131
+ typeof value === "string" && value.match(HBS_REGEX)?.length > 0
132
+ // Parse all values into correct types
133
+ if (operator === "allOr") {
134
+ query.allOr = true
135
+ return
136
+ }
137
+ if (
138
+ type === "datetime" &&
139
+ !isHbs &&
140
+ operator !== "empty" &&
141
+ operator !== "notEmpty"
142
+ ) {
143
+ // Ensure date value is a valid date and parse into correct format
144
+ if (!value) {
145
+ return
146
+ }
147
+ try {
148
+ value = new Date(value).toISOString()
149
+ } catch (error) {
150
+ return
151
+ }
152
+ }
153
+ if (type === "number" && typeof value === "string") {
154
+ if (operator === "oneOf") {
155
+ value = value.split(",").map(item => parseFloat(item))
156
+ } else if (!isHbs) {
157
+ value = parseFloat(value)
158
+ }
159
+ }
160
+ if (type === "boolean") {
161
+ value = `${value}`?.toLowerCase() === "true"
162
+ }
163
+ if (
164
+ ["contains", "notContains", "containsAny"].includes(operator) &&
165
+ type === "array" &&
166
+ typeof value === "string"
167
+ ) {
168
+ value = value.split(",")
169
+ }
170
+ if (operator.startsWith("range")) {
171
+ const minint =
172
+ SqlNumberTypeRangeMap[externalType]?.min || Number.MIN_SAFE_INTEGER
173
+ const maxint =
174
+ SqlNumberTypeRangeMap[externalType]?.max || Number.MAX_SAFE_INTEGER
175
+ if (!query.range[field]) {
176
+ query.range[field] = {
177
+ low: type === "number" ? minint : "0000-00-00T00:00:00.000Z",
178
+ high: type === "number" ? maxint : "9999-00-00T00:00:00.000Z",
179
+ }
180
+ }
181
+ if (operator === "rangeLow" && value != null && value !== "") {
182
+ query.range[field].low = value
183
+ } else if (operator === "rangeHigh" && value != null && value !== "") {
184
+ query.range[field].high = value
185
+ }
186
+ } else if (query[operator]) {
187
+ if (type === "boolean") {
188
+ // Transform boolean filters to cope with null.
189
+ // "equals false" needs to be "not equals true"
190
+ // "not equals false" needs to be "equals true"
191
+ if (operator === "equal" && value === false) {
192
+ query.notEqual[field] = true
193
+ } else if (operator === "notEqual" && value === false) {
194
+ query.equal[field] = true
195
+ } else {
196
+ query[operator][field] = value
197
+ }
198
+ } else {
199
+ query[operator][field] = value
200
+ }
201
+ }
202
+ })
203
+ }
204
+ return query
205
+ }
206
+
207
+ /**
208
+ * Performs a client-side lucene search on an array of data
209
+ * @param docs the data
210
+ * @param query the JSON lucene query
211
+ */
212
+ export const runLuceneQuery = (docs, query) => {
213
+ if (!docs || !Array.isArray(docs)) {
214
+ return []
215
+ }
216
+ if (!query) {
217
+ return docs
218
+ }
219
+
220
+ // Make query consistent first
221
+ query = cleanupQuery(query)
222
+
223
+ // Iterates over a set of filters and evaluates a fail function against a doc
224
+ const match = (type, failFn) => doc => {
225
+ const filters = Object.entries(query[type] || {})
226
+ for (let i = 0; i < filters.length; i++) {
227
+ const [key, testValue] = filters[i]
228
+ const docValue = Helpers.deepGet(doc, removeKeyNumbering(key))
229
+ if (failFn(docValue, testValue)) {
230
+ return false
231
+ }
232
+ }
233
+ return true
234
+ }
235
+
236
+ // Process a string match (fails if the value does not start with the string)
237
+ const stringMatch = match("string", (docValue, testValue) => {
238
+ return (
239
+ !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
240
+ )
241
+ })
242
+
243
+ // Process a fuzzy match (treat the same as starts with when running locally)
244
+ const fuzzyMatch = match("fuzzy", (docValue, testValue) => {
245
+ return (
246
+ !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
247
+ )
248
+ })
249
+
250
+ // Process a range match
251
+ const rangeMatch = match("range", (docValue, testValue) => {
252
+ return (
253
+ docValue == null ||
254
+ docValue === "" ||
255
+ docValue < testValue.low ||
256
+ docValue > testValue.high
257
+ )
258
+ })
259
+
260
+ // Process an equal match (fails if the value is different)
261
+ const equalMatch = match("equal", (docValue, testValue) => {
262
+ return testValue != null && testValue !== "" && docValue !== testValue
263
+ })
264
+
265
+ // Process a not-equal match (fails if the value is the same)
266
+ const notEqualMatch = match("notEqual", (docValue, testValue) => {
267
+ return testValue != null && testValue !== "" && docValue === testValue
268
+ })
269
+
270
+ // Process an empty match (fails if the value is not empty)
271
+ const emptyMatch = match("empty", docValue => {
272
+ return docValue != null && docValue !== ""
273
+ })
274
+
275
+ // Process a not-empty match (fails is the value is empty)
276
+ const notEmptyMatch = match("notEmpty", docValue => {
277
+ return docValue == null || docValue === ""
278
+ })
279
+
280
+ // Process an includes match (fails if the value is not included)
281
+ const oneOf = match("oneOf", (docValue, testValue) => {
282
+ if (typeof testValue === "string") {
283
+ testValue = testValue.split(",")
284
+ if (typeof docValue === "number") {
285
+ testValue = testValue.map(item => parseFloat(item))
286
+ }
287
+ }
288
+ return !testValue?.includes(docValue)
289
+ })
290
+
291
+ const containsAny = match("containsAny", (docValue, testValue) => {
292
+ return !docValue?.includes(...testValue)
293
+ })
294
+
295
+ const contains = match("contains", (docValue, testValue) => {
296
+ return !testValue?.every(item => docValue?.includes(item))
297
+ })
298
+
299
+ const notContains = match("notContains", (docValue, testValue) => {
300
+ return testValue?.every(item => docValue?.includes(item))
301
+ })
302
+
303
+ // Match a document against all criteria
304
+ const docMatch = doc => {
305
+ return (
306
+ stringMatch(doc) &&
307
+ fuzzyMatch(doc) &&
308
+ rangeMatch(doc) &&
309
+ equalMatch(doc) &&
310
+ notEqualMatch(doc) &&
311
+ emptyMatch(doc) &&
312
+ notEmptyMatch(doc) &&
313
+ oneOf(doc) &&
314
+ contains(doc) &&
315
+ containsAny(doc) &&
316
+ notContains(doc)
317
+ )
318
+ }
319
+
320
+ // Process all docs
321
+ return docs.filter(docMatch)
322
+ }
323
+
324
+ /**
325
+ * Performs a client-side sort from the equivalent server-side lucene sort
326
+ * parameters.
327
+ * @param docs the data
328
+ * @param sort the sort column
329
+ * @param sortOrder the sort order ("ascending" or "descending")
330
+ * @param sortType the type of sort ("string" or "number")
331
+ */
332
+ export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
333
+ if (!sort || !sortOrder || !sortType) {
334
+ return docs
335
+ }
336
+ const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
337
+ return docs.slice().sort((a, b) => {
338
+ const colA = parse(a[sort])
339
+ const colB = parse(b[sort])
340
+ if (sortOrder === "Descending") {
341
+ return colA > colB ? -1 : 1
342
+ } else {
343
+ return colA > colB ? 1 : -1
344
+ }
345
+ })
346
+ }
347
+
348
+ /**
349
+ * Limits the specified docs to the specified number of rows from the equivalent
350
+ * server-side lucene limit parameters.
351
+ * @param docs the data
352
+ * @param limit the number of docs to limit to
353
+ */
354
+ export const luceneLimit = (docs, limit) => {
355
+ const numLimit = parseFloat(limit)
356
+ if (isNaN(numLimit)) {
357
+ return docs
358
+ }
359
+ return docs.slice(0, numLimit)
360
+ }