@budibase/shared-core 2.9.19-alpha.0 → 2.9.20

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/src/filters.ts DELETED
@@ -1,431 +0,0 @@
1
- import {
2
- Datasource,
3
- FieldType,
4
- SearchFilter,
5
- SearchQuery,
6
- SearchQueryFields,
7
- SortDirection,
8
- SortType,
9
- } from "@budibase/types"
10
- import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
11
- import { deepGet } from "./helpers"
12
-
13
- const HBS_REGEX = /{{([^{].*?)}}/g
14
-
15
- /**
16
- * Returns the valid operator options for a certain data type
17
- * @param type the data type
18
- */
19
- export const getValidOperatorsForType = (
20
- type: FieldType,
21
- field: string,
22
- datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
23
- ) => {
24
- const Op = OperatorOptions
25
- const stringOps = [
26
- Op.Equals,
27
- Op.NotEquals,
28
- Op.StartsWith,
29
- Op.Like,
30
- Op.Empty,
31
- Op.NotEmpty,
32
- Op.In,
33
- ]
34
- const numOps = [
35
- Op.Equals,
36
- Op.NotEquals,
37
- Op.MoreThan,
38
- Op.LessThan,
39
- Op.Empty,
40
- Op.NotEmpty,
41
- Op.In,
42
- ]
43
- let ops: {
44
- value: string
45
- label: string
46
- }[] = []
47
- if (type === "string") {
48
- ops = stringOps
49
- } else if (type === "number" || type === "bigint") {
50
- ops = numOps
51
- } else if (type === "options") {
52
- ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
53
- } else if (type === "array") {
54
- ops = [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty, Op.ContainsAny]
55
- } else if (type === "boolean") {
56
- ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
57
- } else if (type === "longform") {
58
- ops = stringOps
59
- } else if (type === "datetime") {
60
- ops = numOps
61
- } else if (type === "formula") {
62
- ops = stringOps.concat([Op.MoreThan, Op.LessThan])
63
- }
64
-
65
- // Only allow equal/not equal for _id in SQL tables
66
- const externalTable = datasource?.tableId?.includes("datasource_plus")
67
- if (field === "_id" && externalTable) {
68
- ops = [Op.Equals, Op.NotEquals, Op.In]
69
- }
70
-
71
- return ops
72
- }
73
-
74
- /**
75
- * Operators which do not support empty strings as values
76
- */
77
- export const NoEmptyFilterStrings = [
78
- OperatorOptions.StartsWith.value,
79
- OperatorOptions.Like.value,
80
- OperatorOptions.Equals.value,
81
- OperatorOptions.NotEquals.value,
82
- OperatorOptions.Contains.value,
83
- OperatorOptions.NotContains.value,
84
- ] as (keyof SearchQueryFields)[]
85
-
86
- /**
87
- * Removes any fields that contain empty strings that would cause inconsistent
88
- * behaviour with how backend tables are filtered (no value means no filter).
89
- */
90
- const cleanupQuery = (query: SearchQuery) => {
91
- if (!query) {
92
- return query
93
- }
94
- for (let filterField of NoEmptyFilterStrings) {
95
- if (!query[filterField]) {
96
- continue
97
- }
98
-
99
- for (let [key, value] of Object.entries(query[filterField]!)) {
100
- if (value == null || value === "") {
101
- delete query[filterField]![key]
102
- }
103
- }
104
- }
105
- return query
106
- }
107
-
108
- /**
109
- * Removes a numeric prefix on field names designed to give fields uniqueness
110
- */
111
- const removeKeyNumbering = (key: string) => {
112
- if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
113
- const parts = key.split(":")
114
- parts.shift()
115
- return parts.join(":")
116
- } else {
117
- return key
118
- }
119
- }
120
-
121
- /**
122
- * Builds a lucene JSON query from the filter structure generated in the builder
123
- * @param filter the builder filter structure
124
- */
125
- export const buildLuceneQuery = (filter: SearchFilter[]) => {
126
- let query: SearchQuery = {
127
- string: {},
128
- fuzzy: {},
129
- range: {},
130
- equal: {},
131
- notEqual: {},
132
- empty: {},
133
- notEmpty: {},
134
- contains: {},
135
- notContains: {},
136
- oneOf: {},
137
- containsAny: {},
138
- }
139
- if (Array.isArray(filter)) {
140
- filter.forEach(expression => {
141
- let { operator, field, type, value, externalType } = expression
142
- const isHbs =
143
- typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
144
- // Parse all values into correct types
145
- if (operator === "allOr") {
146
- query.allOr = true
147
- return
148
- }
149
- if (
150
- type === "datetime" &&
151
- !isHbs &&
152
- operator !== "empty" &&
153
- operator !== "notEmpty"
154
- ) {
155
- // Ensure date value is a valid date and parse into correct format
156
- if (!value) {
157
- return
158
- }
159
- try {
160
- value = new Date(value).toISOString()
161
- } catch (error) {
162
- return
163
- }
164
- }
165
- if (type === "number" && typeof value === "string") {
166
- if (operator === "oneOf") {
167
- value = value.split(",").map(item => parseFloat(item))
168
- } else if (!isHbs) {
169
- value = parseFloat(value)
170
- }
171
- }
172
- if (type === "boolean") {
173
- value = `${value}`?.toLowerCase() === "true"
174
- }
175
- if (
176
- ["contains", "notContains", "containsAny"].includes(operator) &&
177
- type === "array" &&
178
- typeof value === "string"
179
- ) {
180
- value = value.split(",")
181
- }
182
- if (operator.startsWith("range") && query.range) {
183
- const minint =
184
- SqlNumberTypeRangeMap[
185
- externalType as keyof typeof SqlNumberTypeRangeMap
186
- ]?.min || Number.MIN_SAFE_INTEGER
187
- const maxint =
188
- SqlNumberTypeRangeMap[
189
- externalType as keyof typeof SqlNumberTypeRangeMap
190
- ]?.max || Number.MAX_SAFE_INTEGER
191
- if (!query.range[field]) {
192
- query.range[field] = {
193
- low: type === "number" ? minint : "0000-00-00T00:00:00.000Z",
194
- high: type === "number" ? maxint : "9999-00-00T00:00:00.000Z",
195
- }
196
- }
197
- if ((operator as any) === "rangeLow" && value != null && value !== "") {
198
- query.range[field].low = value
199
- } else if (
200
- (operator as any) === "rangeHigh" &&
201
- value != null &&
202
- value !== ""
203
- ) {
204
- query.range[field].high = value
205
- }
206
- } else if (query[operator]) {
207
- if (type === "boolean") {
208
- // Transform boolean filters to cope with null.
209
- // "equals false" needs to be "not equals true"
210
- // "not equals false" needs to be "equals true"
211
- if (operator === "equal" && value === false) {
212
- query.notEqual = query.notEqual || {}
213
- query.notEqual[field] = true
214
- } else if (operator === "notEqual" && value === false) {
215
- query.equal = query.equal || {}
216
- query.equal[field] = true
217
- } else {
218
- query[operator] = query[operator] || {}
219
- query[operator]![field] = value
220
- }
221
- } else {
222
- query[operator] = query[operator] || {}
223
- query[operator]![field] = value
224
- }
225
- }
226
- })
227
- }
228
- return query
229
- }
230
-
231
- /**
232
- * Performs a client-side lucene search on an array of data
233
- * @param docs the data
234
- * @param query the JSON lucene query
235
- */
236
- export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
237
- if (!docs || !Array.isArray(docs)) {
238
- return []
239
- }
240
- if (!query) {
241
- return docs
242
- }
243
-
244
- // Make query consistent first
245
- query = cleanupQuery(query)
246
-
247
- // Iterates over a set of filters and evaluates a fail function against a doc
248
- const match =
249
- (
250
- type: keyof SearchQueryFields,
251
- failFn: (docValue: any, testValue: any) => boolean
252
- ) =>
253
- (doc: any) => {
254
- const filters = Object.entries(query![type] || {})
255
- for (let i = 0; i < filters.length; i++) {
256
- const [key, testValue] = filters[i]
257
- const docValue = deepGet(doc, removeKeyNumbering(key))
258
- if (failFn(docValue, testValue)) {
259
- return false
260
- }
261
- }
262
- return true
263
- }
264
-
265
- // Process a string match (fails if the value does not start with the string)
266
- const stringMatch = match("string", (docValue: string, testValue: string) => {
267
- return (
268
- !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
269
- )
270
- })
271
-
272
- // Process a fuzzy match (treat the same as starts with when running locally)
273
- const fuzzyMatch = match("fuzzy", (docValue: string, testValue: string) => {
274
- return (
275
- !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
276
- )
277
- })
278
-
279
- // Process a range match
280
- const rangeMatch = match(
281
- "range",
282
- (
283
- docValue: string | number | null,
284
- testValue: { low: number; high: number }
285
- ) => {
286
- return (
287
- docValue == null ||
288
- docValue === "" ||
289
- +docValue < testValue.low ||
290
- +docValue > testValue.high
291
- )
292
- }
293
- )
294
-
295
- // Process an equal match (fails if the value is different)
296
- const equalMatch = match(
297
- "equal",
298
- (docValue: any, testValue: string | null) => {
299
- return testValue != null && testValue !== "" && docValue !== testValue
300
- }
301
- )
302
-
303
- // Process a not-equal match (fails if the value is the same)
304
- const notEqualMatch = match(
305
- "notEqual",
306
- (docValue: any, testValue: string | null) => {
307
- return testValue != null && testValue !== "" && docValue === testValue
308
- }
309
- )
310
-
311
- // Process an empty match (fails if the value is not empty)
312
- const emptyMatch = match("empty", (docValue: string | null) => {
313
- return docValue != null && docValue !== ""
314
- })
315
-
316
- // Process a not-empty match (fails is the value is empty)
317
- const notEmptyMatch = match("notEmpty", (docValue: string | null) => {
318
- return docValue == null || docValue === ""
319
- })
320
-
321
- // Process an includes match (fails if the value is not included)
322
- const oneOf = match("oneOf", (docValue: any, testValue: any) => {
323
- if (typeof testValue === "string") {
324
- testValue = testValue.split(",")
325
- if (typeof docValue === "number") {
326
- testValue = testValue.map((item: string) => parseFloat(item))
327
- }
328
- }
329
- return !testValue?.includes(docValue)
330
- })
331
-
332
- const containsAny = match("containsAny", (docValue: any, testValue: any) => {
333
- return !docValue?.includes(...testValue)
334
- })
335
-
336
- const contains = match(
337
- "contains",
338
- (docValue: string | any[], testValue: any[]) => {
339
- return !testValue?.every((item: any) => docValue?.includes(item))
340
- }
341
- )
342
-
343
- const notContains = match(
344
- "notContains",
345
- (docValue: string | any[], testValue: any[]) => {
346
- return testValue?.every((item: any) => docValue?.includes(item))
347
- }
348
- )
349
-
350
- // Match a document against all criteria
351
- const docMatch = (doc: any) => {
352
- return (
353
- stringMatch(doc) &&
354
- fuzzyMatch(doc) &&
355
- rangeMatch(doc) &&
356
- equalMatch(doc) &&
357
- notEqualMatch(doc) &&
358
- emptyMatch(doc) &&
359
- notEmptyMatch(doc) &&
360
- oneOf(doc) &&
361
- contains(doc) &&
362
- containsAny(doc) &&
363
- notContains(doc)
364
- )
365
- }
366
-
367
- // Process all docs
368
- return docs.filter(docMatch)
369
- }
370
-
371
- /**
372
- * Performs a client-side sort from the equivalent server-side lucene sort
373
- * parameters.
374
- * @param docs the data
375
- * @param sort the sort column
376
- * @param sortOrder the sort order ("ascending" or "descending")
377
- * @param sortType the type of sort ("string" or "number")
378
- */
379
- export const luceneSort = (
380
- docs: any[],
381
- sort: string,
382
- sortOrder: SortDirection,
383
- sortType = SortType.STRING
384
- ) => {
385
- if (!sort || !sortOrder || !sortType) {
386
- return docs
387
- }
388
- const parse =
389
- sortType === "string" ? (x: any) => `${x}` : (x: string) => parseFloat(x)
390
- return docs
391
- .slice()
392
- .sort((a: { [x: string]: any }, b: { [x: string]: any }) => {
393
- const colA = parse(a[sort])
394
- const colB = parse(b[sort])
395
- if (sortOrder.toLowerCase() === "descending") {
396
- return colA > colB ? -1 : 1
397
- } else {
398
- return colA > colB ? 1 : -1
399
- }
400
- })
401
- }
402
-
403
- /**
404
- * Limits the specified docs to the specified number of rows from the equivalent
405
- * server-side lucene limit parameters.
406
- * @param docs the data
407
- * @param limit the number of docs to limit to
408
- */
409
- export const luceneLimit = (docs: any[], limit: string) => {
410
- const numLimit = parseFloat(limit)
411
- if (isNaN(numLimit)) {
412
- return docs
413
- }
414
- return docs.slice(0, numLimit)
415
- }
416
-
417
- export const hasFilters = (query?: SearchQuery) => {
418
- if (!query) {
419
- return false
420
- }
421
- const skipped = ["allOr"]
422
- for (let [key, value] of Object.entries(query)) {
423
- if (skipped.includes(key) || typeof value !== "object") {
424
- continue
425
- }
426
- if (Object.keys(value).length !== 0) {
427
- return true
428
- }
429
- }
430
- return false
431
- }
@@ -1,85 +0,0 @@
1
- import { User } from "@budibase/types"
2
-
3
- /**
4
- * Gets a key within an object. The key supports dot syntax for retrieving deep
5
- * fields - e.g. "a.b.c".
6
- * Exact matches of keys with dots in them take precedence over nested keys of
7
- * the same path - e.g. getting "a.b" from { "a.b": "foo", a: { b: "bar" } }
8
- * will return "foo" over "bar".
9
- * @param obj the object
10
- * @param key the key
11
- * @return {*|null} the value or null if a value was not found for this key
12
- */
13
- export const deepGet = (obj: { [x: string]: any }, key: string) => {
14
- if (!obj || !key) {
15
- return null
16
- }
17
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
18
- return obj[key]
19
- }
20
- const split = key.split(".")
21
- for (let i = 0; i < split.length; i++) {
22
- obj = obj?.[split[i]]
23
- }
24
- return obj
25
- }
26
-
27
- /**
28
- * Gets the initials to show in a user avatar.
29
- * @param user the user
30
- */
31
- export const getUserInitials = (user: User) => {
32
- if (!user) {
33
- return "?"
34
- }
35
- let initials = ""
36
- initials += user.firstName ? user.firstName[0] : ""
37
- initials += user.lastName ? user.lastName[0] : ""
38
- if (initials !== "") {
39
- return initials
40
- }
41
- return user.email?.[0] || "U"
42
- }
43
-
44
- /**
45
- * Gets a deterministic colour for a particular user
46
- * @param user the user
47
- */
48
- export const getUserColor = (user: User) => {
49
- let id = user?._id
50
- if (!id) {
51
- return "var(--spectrum-global-color-blue-400)"
52
- }
53
-
54
- // In order to generate the same color for global users as app users, we need
55
- // to remove the app-specific table prefix
56
- id = id.replace("ro_ta_users_", "")
57
-
58
- // Generate a hue based on the ID
59
- let hue = 1
60
- for (let i = 0; i < id.length; i++) {
61
- hue += id.charCodeAt(i)
62
- hue = hue % 36
63
- }
64
- return `hsl(${hue * 10}, 50%, 40%)`
65
- }
66
-
67
- /**
68
- * Gets a friendly label to describe who a user is.
69
- * @param user the user
70
- */
71
- export const getUserLabel = (user: User) => {
72
- if (!user) {
73
- return ""
74
- }
75
- const { firstName, lastName, email } = user
76
- if (firstName && lastName) {
77
- return `${firstName} ${lastName}`
78
- } else if (firstName) {
79
- return firstName
80
- } else if (lastName) {
81
- return lastName
82
- } else {
83
- return email
84
- }
85
- }
@@ -1,2 +0,0 @@
1
- export * from "./helpers"
2
- export * from "./integrations"
@@ -1,18 +0,0 @@
1
- import { Datasource, SourceName } from "@budibase/types"
2
-
3
- export function isGoogleSheets(type: SourceName) {
4
- return type === SourceName.GOOGLE_SHEETS
5
- }
6
-
7
- export function isSQL(datasource: Datasource): boolean {
8
- if (!datasource || !datasource.source) {
9
- return false
10
- }
11
- const SQL = [
12
- SourceName.POSTGRES,
13
- SourceName.SQL_SERVER,
14
- SourceName.MYSQL,
15
- SourceName.ORACLE,
16
- ]
17
- return SQL.indexOf(datasource.source) !== -1
18
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./constants"
2
- export * as dataFilters from "./filters"
3
- export * as helpers from "./helpers"
4
- export * as utils from "./utils"
5
- export * as sdk from "./sdk"
@@ -1,35 +0,0 @@
1
- import { DocumentType, prefixed } from "@budibase/types"
2
-
3
- const APP_PREFIX = prefixed(DocumentType.APP)
4
- const APP_DEV_PREFIX = prefixed(DocumentType.APP_DEV)
5
-
6
- export function getDevAppID(appId: string) {
7
- if (!appId) {
8
- throw new Error("No app ID provided")
9
- }
10
- if (appId.startsWith(APP_DEV_PREFIX)) {
11
- return appId
12
- }
13
- // split to take off the app_ element, then join it together incase any other app_ exist
14
- const split = appId.split(APP_PREFIX)
15
- split.shift()
16
- const rest = split.join(APP_PREFIX)
17
- return `${APP_DEV_PREFIX}${rest}`
18
- }
19
-
20
- /**
21
- * Convert a development app ID to a deployed app ID.
22
- */
23
- export function getProdAppID(appId: string) {
24
- if (!appId) {
25
- throw new Error("No app ID provided")
26
- }
27
- if (!appId.startsWith(APP_DEV_PREFIX)) {
28
- return appId
29
- }
30
- // split to take off the app_dev element, then join it together incase any other app_ exist
31
- const split = appId.split(APP_DEV_PREFIX)
32
- split.shift()
33
- const rest = split.join(APP_DEV_PREFIX)
34
- return `${APP_PREFIX}${rest}`
35
- }
@@ -1,2 +0,0 @@
1
- export * as applications from "./applications"
2
- export * as users from "./users"
@@ -1,62 +0,0 @@
1
- import { ContextUser, User } from "@budibase/types"
2
- import { getProdAppID } from "./applications"
3
-
4
- // checks if a user is specifically a builder, given an app ID
5
- export function isBuilder(user: User | ContextUser, appId?: string): boolean {
6
- if (!user) {
7
- return false
8
- }
9
- if (user.builder?.global) {
10
- return true
11
- } else if (appId && user.builder?.apps?.includes(getProdAppID(appId))) {
12
- return true
13
- }
14
- return false
15
- }
16
-
17
- export function isGlobalBuilder(user: User | ContextUser): boolean {
18
- return (isBuilder(user) && !hasAppBuilderPermissions(user)) || isAdmin(user)
19
- }
20
-
21
- // alias for hasAdminPermission, currently do the same thing
22
- // in future whether someone has admin permissions and whether they are
23
- // an admin for a specific resource could be separated
24
- export function isAdmin(user: User | ContextUser): boolean {
25
- if (!user) {
26
- return false
27
- }
28
- return hasAdminPermissions(user)
29
- }
30
-
31
- export function isAdminOrBuilder(
32
- user: User | ContextUser,
33
- appId?: string
34
- ): boolean {
35
- return isBuilder(user, appId) || isAdmin(user)
36
- }
37
-
38
- // check if they are a builder within an app (not necessarily a global builder)
39
- export function hasAppBuilderPermissions(user?: User | ContextUser): boolean {
40
- if (!user) {
41
- return false
42
- }
43
- const appLength = user.builder?.apps?.length
44
- const isGlobalBuilder = !!user.builder?.global
45
- return !isGlobalBuilder && appLength != null && appLength > 0
46
- }
47
-
48
- // checks if a user is capable of building any app
49
- export function hasBuilderPermissions(user?: User | ContextUser): boolean {
50
- if (!user) {
51
- return false
52
- }
53
- return user.builder?.global || hasAppBuilderPermissions(user)
54
- }
55
-
56
- // checks if a user is capable of being an admin
57
- export function hasAdminPermissions(user?: User | ContextUser): boolean {
58
- if (!user) {
59
- return false
60
- }
61
- return !!user.admin?.global
62
- }
package/src/sdk/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./documents"
package/src/utils.ts DELETED
@@ -1,45 +0,0 @@
1
- export function unreachable(
2
- value: never,
3
- message = `No such case in exhaustive switch: ${value}`
4
- ) {
5
- throw new Error(message)
6
- }
7
-
8
- export async function parallelForeach<T>(
9
- items: T[],
10
- task: (item: T) => Promise<void>,
11
- maxConcurrency: number
12
- ): Promise<void> {
13
- const promises: Promise<void>[] = []
14
- let index = 0
15
-
16
- const processItem = async (item: T) => {
17
- try {
18
- await task(item)
19
- } finally {
20
- processNext()
21
- }
22
- }
23
-
24
- const processNext = () => {
25
- if (index >= items.length) {
26
- // No more items to process
27
- return
28
- }
29
-
30
- const item = items[index]
31
- index++
32
-
33
- const promise = processItem(item)
34
- promises.push(promise)
35
-
36
- if (promises.length >= maxConcurrency) {
37
- Promise.race(promises).then(processNext)
38
- } else {
39
- processNext()
40
- }
41
- }
42
- processNext()
43
-
44
- await Promise.all(promises)
45
- }