2024-09-30 19:06:47 +02:00
|
|
|
import {
|
|
|
|
LegacyFilter,
|
|
|
|
SearchFilterGroup,
|
|
|
|
FilterGroupLogicalOperator,
|
|
|
|
SearchFilters,
|
|
|
|
BasicOperator,
|
|
|
|
ArrayOperator,
|
2024-10-09 10:33:15 +02:00
|
|
|
isLogicalSearchOperator,
|
2024-09-30 19:06:47 +02:00
|
|
|
} from "@budibase/types"
|
2023-11-07 16:07:00 +01:00
|
|
|
import * as Constants from "./constants"
|
2024-09-30 19:06:47 +02:00
|
|
|
import { removeKeyNumbering } from "./filters"
|
|
|
|
|
|
|
|
// an array of keys from filter type to properties that are in the type
|
|
|
|
// this can then be converted using .fromEntries to an object
|
2024-10-01 12:31:41 +02:00
|
|
|
type AllowedFilters = [keyof LegacyFilter, LegacyFilter[keyof LegacyFilter]][]
|
2023-11-07 16:07:00 +01:00
|
|
|
|
2023-03-09 09:50:26 +01:00
|
|
|
export function unreachable(
|
|
|
|
value: never,
|
|
|
|
message = `No such case in exhaustive switch: ${value}`
|
|
|
|
) {
|
|
|
|
throw new Error(message)
|
|
|
|
}
|
2023-06-07 13:29:36 +02:00
|
|
|
|
|
|
|
export async function parallelForeach<T>(
|
|
|
|
items: T[],
|
|
|
|
task: (item: T) => Promise<void>,
|
|
|
|
maxConcurrency: number
|
|
|
|
): Promise<void> {
|
|
|
|
const promises: Promise<void>[] = []
|
|
|
|
let index = 0
|
|
|
|
|
|
|
|
const processItem = async (item: T) => {
|
|
|
|
try {
|
|
|
|
await task(item)
|
|
|
|
} finally {
|
|
|
|
processNext()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const processNext = () => {
|
|
|
|
if (index >= items.length) {
|
|
|
|
// No more items to process
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const item = items[index]
|
|
|
|
index++
|
|
|
|
|
|
|
|
const promise = processItem(item)
|
|
|
|
promises.push(promise)
|
|
|
|
|
|
|
|
if (promises.length >= maxConcurrency) {
|
|
|
|
Promise.race(promises).then(processNext)
|
|
|
|
} else {
|
|
|
|
processNext()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
processNext()
|
|
|
|
|
|
|
|
await Promise.all(promises)
|
|
|
|
}
|
2023-11-07 16:07:00 +01:00
|
|
|
|
|
|
|
export function filterValueToLabel() {
|
|
|
|
return Object.keys(Constants.OperatorOptions).reduce(
|
|
|
|
(acc: { [key: string]: string }, key: string) => {
|
|
|
|
const ops: { [key: string]: any } = Constants.OperatorOptions
|
|
|
|
const op: { [key: string]: string } = ops[key]
|
|
|
|
acc[op["value"]] = op.label
|
|
|
|
return acc
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
}
|
2024-02-19 10:13:03 +01:00
|
|
|
|
|
|
|
export function hasSchema(test: any) {
|
|
|
|
return (
|
|
|
|
typeof test === "object" &&
|
|
|
|
!Array.isArray(test) &&
|
|
|
|
test !== null &&
|
|
|
|
!(test instanceof Date) &&
|
|
|
|
Object.keys(test).length > 0
|
|
|
|
)
|
|
|
|
}
|
2024-08-01 11:02:21 +02:00
|
|
|
|
|
|
|
export function trimOtherProps(object: any, allowedProps: string[]) {
|
|
|
|
const result = Object.keys(object)
|
|
|
|
.filter(key => allowedProps.includes(key))
|
|
|
|
.reduce<Record<string, any>>(
|
|
|
|
(acc, key) => ({ ...acc, [key]: object[key] }),
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
return result
|
|
|
|
}
|
2024-09-30 16:16:24 +02:00
|
|
|
|
|
|
|
export function isSupportedUserSearch(query: SearchFilters) {
|
|
|
|
const allowed = [
|
|
|
|
{ op: BasicOperator.STRING, key: "email" },
|
|
|
|
{ op: BasicOperator.EQUAL, key: "_id" },
|
|
|
|
{ op: ArrayOperator.ONE_OF, key: "_id" },
|
|
|
|
]
|
2024-10-09 10:33:15 +02:00
|
|
|
for (const [key, operation] of Object.entries(query)) {
|
2024-09-30 16:16:24 +02:00
|
|
|
if (typeof operation !== "object") {
|
|
|
|
return false
|
|
|
|
}
|
2024-10-09 10:33:15 +02:00
|
|
|
|
|
|
|
if (isLogicalSearchOperator(key)) {
|
|
|
|
for (const condition of query[key]!.conditions) {
|
|
|
|
if (!isSupportedUserSearch(condition)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2024-09-30 16:16:24 +02:00
|
|
|
const fields = Object.keys(operation || {})
|
|
|
|
// this filter doesn't contain options - ignore
|
|
|
|
if (fields.length === 0) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const allowedOperation = allowed.find(
|
|
|
|
allow =>
|
|
|
|
allow.op === key && fields.length === 1 && fields[0] === allow.key
|
|
|
|
)
|
|
|
|
if (!allowedOperation) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
2024-09-30 19:06:47 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Processes the filter config. Filters are migrated from
|
|
|
|
* SearchFilter[] to SearchFilterGroup
|
|
|
|
*
|
|
|
|
* If filters is not an array, the migration is skipped
|
|
|
|
*
|
|
|
|
* @param {LegacyFilter[] | SearchFilterGroup} filters
|
|
|
|
*/
|
|
|
|
export const processSearchFilters = (
|
|
|
|
filters: LegacyFilter[] | SearchFilterGroup | undefined
|
|
|
|
): SearchFilterGroup | undefined => {
|
|
|
|
if (!filters) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Base search config.
|
|
|
|
const defaultCfg: SearchFilterGroup = {
|
|
|
|
logicalOperator: FilterGroupLogicalOperator.ALL,
|
|
|
|
groups: [],
|
|
|
|
}
|
|
|
|
|
2024-10-01 12:31:41 +02:00
|
|
|
const filterAllowedKeys = [
|
2024-09-30 19:06:47 +02:00
|
|
|
"field",
|
|
|
|
"operator",
|
|
|
|
"value",
|
|
|
|
"type",
|
|
|
|
"externalType",
|
|
|
|
"valueType",
|
|
|
|
"noValue",
|
|
|
|
"formulaType",
|
|
|
|
]
|
|
|
|
|
|
|
|
if (Array.isArray(filters)) {
|
|
|
|
let baseGroup: SearchFilterGroup = {
|
|
|
|
filters: [],
|
|
|
|
logicalOperator: FilterGroupLogicalOperator.ALL,
|
|
|
|
}
|
|
|
|
|
|
|
|
return filters.reduce((acc: SearchFilterGroup, filter: LegacyFilter) => {
|
|
|
|
// Sort the properties for easier debugging
|
|
|
|
const filterPropertyKeys = (Object.keys(filter) as (keyof LegacyFilter)[])
|
|
|
|
.sort((a, b) => {
|
|
|
|
return a.localeCompare(b)
|
|
|
|
})
|
2024-10-11 16:41:51 +02:00
|
|
|
.filter(key => filter[key])
|
2024-09-30 19:06:47 +02:00
|
|
|
|
|
|
|
if (filterPropertyKeys.length == 1) {
|
|
|
|
const key = filterPropertyKeys[0],
|
|
|
|
value = filter[key]
|
|
|
|
// Global
|
|
|
|
if (key === "onEmptyFilter") {
|
|
|
|
// unset otherwise
|
|
|
|
acc.onEmptyFilter = value
|
|
|
|
} else if (key === "operator" && value === "allOr") {
|
|
|
|
// Group 1 logical operator
|
|
|
|
baseGroup.logicalOperator = FilterGroupLogicalOperator.ANY
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc
|
|
|
|
}
|
|
|
|
|
2024-10-01 12:31:41 +02:00
|
|
|
const allowedFilterSettings: AllowedFilters = filterPropertyKeys.reduce(
|
|
|
|
(acc: AllowedFilters, key) => {
|
2024-09-30 19:06:47 +02:00
|
|
|
const value = filter[key]
|
2024-10-01 12:31:41 +02:00
|
|
|
if (filterAllowedKeys.includes(key)) {
|
2024-09-30 19:06:47 +02:00
|
|
|
if (key === "field") {
|
|
|
|
acc.push([key, removeKeyNumbering(value)])
|
|
|
|
} else {
|
|
|
|
acc.push([key, value])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return acc
|
2024-10-01 12:31:41 +02:00
|
|
|
},
|
|
|
|
[]
|
|
|
|
)
|
2024-09-30 19:06:47 +02:00
|
|
|
|
|
|
|
const migratedFilter: LegacyFilter = Object.fromEntries(
|
2024-10-01 12:31:41 +02:00
|
|
|
allowedFilterSettings
|
2024-09-30 19:06:47 +02:00
|
|
|
) as LegacyFilter
|
|
|
|
|
|
|
|
baseGroup.filters!.push(migratedFilter)
|
|
|
|
|
|
|
|
if (!acc.groups || !acc.groups.length) {
|
|
|
|
// init the base group
|
|
|
|
acc.groups = [baseGroup]
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc
|
|
|
|
}, defaultCfg)
|
|
|
|
} else if (!filters?.groups) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return filters
|
|
|
|
}
|