Fix allOr for everything except SQS.

This commit is contained in:
Sam Rose 2024-10-18 14:50:06 +01:00
parent 3981cecc30
commit 0fce17c3c0
No known key found for this signature in database
5 changed files with 139 additions and 134 deletions

View File

@ -3756,6 +3756,26 @@ describe.each([
}), }),
expected: [], expected: [],
}, },
{
name: "allOr",
insert: [{ string: "foo" }, { string: "bar" }],
query: simpleQuery(
{
operator: BasicOperator.EQUAL,
field: "string",
value: "foo",
},
{
operator: BasicOperator.EQUAL,
field: "string",
value: "bar",
},
{
operator: "allOr",
}
),
expected: [{ string: "foo" }, { string: "bar" }],
},
] ]
it.only.each(testCases)( it.only.each(testCases)(

View File

@ -114,11 +114,12 @@ export async function search(
? view.query ? view.query
: [] : []
const { filters } = dataFilters.splitFiltersArray(queryFilters)
// Extract existing fields // Extract existing fields
const existingFields = const existingFields = filters.map(filter =>
queryFilters db.removeKeyNumbering(filter.field)
?.filter(filter => filter.field) )
.map(filter => db.removeKeyNumbering(filter.field)) || []
// Carry over filters for unused fields // Carry over filters for unused fields
Object.keys(options.query).forEach(key => { Object.keys(options.query).forEach(key => {

View File

@ -24,6 +24,7 @@ import {
isBasicSearchOperator, isBasicSearchOperator,
isArraySearchOperator, isArraySearchOperator,
isRangeSearchOperator, isRangeSearchOperator,
SearchFilter,
} from "@budibase/types" } from "@budibase/types"
import dayjs from "dayjs" import dayjs from "dayjs"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants" import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
@ -310,24 +311,17 @@ export class ColumnSplitter {
* Builds a JSON query from the filter a SearchFilter definition * Builds a JSON query from the filter a SearchFilter definition
* @param filter the builder filter structure * @param filter the builder filter structure
*/ */
const buildCondition = (expression: LegacyFilter) => {
function buildCondition(filter: undefined): undefined
function buildCondition(filter: SearchFilter): SearchFilters
function buildCondition(filter?: SearchFilter): SearchFilters | undefined {
if (!filter) {
return
}
const query: SearchFilters = {} const query: SearchFilters = {}
const { operator, field, type, externalType, onEmptyFilter } = expression const { operator, field, type, externalType } = filter
let { value } = expression let { value } = filter
if (!operator || !field) {
return
}
if (operator === "allOr") {
query.allOr = true
return
}
if (onEmptyFilter) {
query.onEmptyFilter = onEmptyFilter
return
}
// Default the value for noValue fields to ensure they are correctly added // Default the value for noValue fields to ensure they are correctly added
// to the final query // to the final query
@ -432,16 +426,36 @@ const buildCondition = (expression: LegacyFilter) => {
return query return query
} }
export interface LegacyFilterSplit {
allOr?: boolean
onEmptyFilter?: EmptyFilterOption
filters: SearchFilter[]
}
export function splitFiltersArray(filters: LegacyFilter[]) {
const split: LegacyFilterSplit = {
filters: [],
}
for (const filter of filters) {
if ("operator" in filter && filter.operator === "allOr") {
split.allOr = true
} else if ("onEmptyFilter" in filter) {
split.onEmptyFilter = filter.onEmptyFilter
} else {
split.filters.push(filter)
}
}
return split
}
/** /**
* Converts a **UISearchFilter** filter definition into a grouped * Converts a **UISearchFilter** filter definition into a grouped
* search query of type **SearchFilters** * search query of type **SearchFilters**
* *
* Legacy support remains for the old **SearchFilter[]** format. * Legacy support remains for the old **SearchFilter[]** format.
* These will be migrated to an appropriate **SearchFilters** object, if encountered * These will be migrated to an appropriate **SearchFilters** object, if encountered
*
* @param filter
*
* @returns {SearchFilters}
*/ */
export function buildQuery(filter: undefined): undefined export function buildQuery(filter: undefined): undefined
export function buildQuery( export function buildQuery(
@ -454,26 +468,33 @@ export function buildQuery(
return return
} }
let parsedFilter: UISearchFilter
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
parsedFilter = processSearchFilters(filter) filter = processSearchFilters(filter)
} else {
parsedFilter = filter
} }
const operator = logicalOperatorFromUI( const operator = logicalOperatorFromUI(
parsedFilter.logicalOperator || UILogicalOperator.ALL filter.logicalOperator || UILogicalOperator.ALL
) )
const groups = parsedFilter.groups || []
const conditions: SearchFilters[] = groups.map(group => {
const filters = group.filters || []
return { [operator]: { conditions: filters.map(x => buildCondition(x)) } }
})
return { const query: SearchFilters = {}
onEmptyFilter: parsedFilter.onEmptyFilter, if (filter.onEmptyFilter) {
[operator]: { conditions }, query.onEmptyFilter = filter.onEmptyFilter
} }
query[operator] = {
conditions: (filter.groups || []).map(group => {
const { allOr, onEmptyFilter, filters } = splitFiltersArray(
group.filters || []
)
if (onEmptyFilter) {
query.onEmptyFilter = onEmptyFilter
}
const operator = allOr ? LogicalOperator.OR : LogicalOperator.AND
return { [operator]: { conditions: filters.map(buildCondition) } }
}),
}
return query
} }
function logicalOperatorFromUI(operator: UILogicalOperator): LogicalOperator { function logicalOperatorFromUI(operator: UILogicalOperator): LogicalOperator {

View File

@ -6,15 +6,22 @@ import {
BasicOperator, BasicOperator,
ArrayOperator, ArrayOperator,
isLogicalSearchOperator, isLogicalSearchOperator,
EmptyFilterOption, SearchFilter,
SearchFilterGroup,
} from "@budibase/types" } from "@budibase/types"
import * as Constants from "./constants" import * as Constants from "./constants"
import { removeKeyNumbering } from "./filters" import { removeKeyNumbering, splitFiltersArray } from "./filters"
import _ from "lodash"
// an array of keys from filter type to properties that are in the type const FILTER_ALLOWED_KEYS = [
// this can then be converted using .fromEntries to an object "field",
type AllowedFilters = [keyof LegacyFilter, LegacyFilter[keyof LegacyFilter]][] "operator",
"value",
"type",
"externalType",
"valueType",
"noValue",
"formulaType",
]
export function unreachable( export function unreachable(
value: never, value: never,
@ -130,88 +137,20 @@ export function isSupportedUserSearch(query: SearchFilters) {
return true return true
} }
/**
* Processes the filter config. Filters are migrated from
* SearchFilter[] to UISearchFilter
*
* If filters is not an array, the migration is skipped
*
* @param {LegacyFilter[] | UISearchFilter} filters
*/
export const processSearchFilters = ( export const processSearchFilters = (
filters: LegacyFilter[] filterArray: LegacyFilter[]
): UISearchFilter => { ): UISearchFilter => {
// Base search config. const { allOr, onEmptyFilter, filters } = splitFiltersArray(filterArray)
const defaultCfg: UISearchFilter = { return {
logicalOperator: UILogicalOperator.ALL, onEmptyFilter,
onEmptyFilter: EmptyFilterOption.RETURN_ALL, groups: [
groups: [], {
} logicalOperator: allOr ? UILogicalOperator.ANY : UILogicalOperator.ALL,
filters: filters.map(filter => {
const filterAllowedKeys = [ filter.field = removeKeyNumbering(filter.field)
"field", return _.pick(filter, FILTER_ALLOWED_KEYS) as SearchFilter
"operator", }),
"value",
"type",
"externalType",
"valueType",
"noValue",
"formulaType",
]
let baseGroup: SearchFilterGroup = {
logicalOperator: UILogicalOperator.ALL,
}
return filters.reduce((acc: UISearchFilter, filter: LegacyFilter) => {
// Sort the properties for easier debugging
const filterPropertyKeys = (Object.keys(filter) as (keyof LegacyFilter)[])
.sort((a, b) => {
return a.localeCompare(b)
})
.filter(key => filter[key])
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 = UILogicalOperator.ANY
}
return acc
}
const allowedFilterSettings: AllowedFilters = filterPropertyKeys.reduce(
(acc: AllowedFilters, key) => {
const value = filter[key]
if (filterAllowedKeys.includes(key)) {
if (key === "field") {
acc.push([key, removeKeyNumbering(value)])
} else {
acc.push([key, value])
}
}
return acc
}, },
[] ],
)
const migratedFilter: LegacyFilter = Object.fromEntries(
allowedFilterSettings
) as LegacyFilter
baseGroup.filters!.push(migratedFilter)
if (!acc.groups || !acc.groups.length) {
// init the base group
acc.groups = [baseGroup]
} }
return acc
}, defaultCfg)
} }

View File

@ -1,16 +1,40 @@
import { FieldType } from "../../documents" import { FieldType } from "../../documents"
import { EmptyFilterOption, UILogicalOperator, SearchFilters } from "../../sdk" import {
EmptyFilterOption,
UILogicalOperator,
BasicOperator,
RangeOperator,
ArrayOperator,
} from "../../sdk"
type AllOr = {
operator: "allOr"
}
type OnEmptyFilter = {
onEmptyFilter: EmptyFilterOption
}
// TODO(samwho): this could be broken down further
export type SearchFilter = {
operator:
| BasicOperator
| RangeOperator
| ArrayOperator
| "rangeLow"
| "rangeHigh"
// Field name will often have a numerical prefix when coming from the frontend,
// use the ColumnSplitter class to remove it.
field: string
value: any
type?: FieldType
externalType?: string
noValue?: boolean
}
// Prior to v2, this is the type the frontend sent us when filters were // Prior to v2, this is the type the frontend sent us when filters were
// involved. We convert this to a SearchFilters before use with the search SDK. // involved. We convert this to a SearchFilters before use with the search SDK.
export type LegacyFilter = { export type LegacyFilter = AllOr | OnEmptyFilter | SearchFilter
operator: keyof SearchFilters | "rangeLow" | "rangeHigh"
onEmptyFilter?: EmptyFilterOption
field: string
type?: FieldType
value: any
externalType?: string
}
export type SearchFilterGroup = { export type SearchFilterGroup = {
logicalOperator?: UILogicalOperator logicalOperator?: UILogicalOperator