Refactor SearchFilterGroup type to be less verbose and more clearly named.

This commit is contained in:
Sam Rose 2024-10-18 10:32:28 +01:00
parent 714029b9a0
commit b7979f4719
No known key found for this signature in database
7 changed files with 73 additions and 108 deletions

View File

@ -27,20 +27,18 @@ import {
ViewV2Schema, ViewV2Schema,
ViewV2Type, ViewV2Type,
JsonTypes, JsonTypes,
FilterGroupLogicalOperator, UILogicalOperator,
EmptyFilterOption, EmptyFilterOption,
JsonFieldSubType, JsonFieldSubType,
SearchFilterGroup, UISearchFilter,
LegacyFilter, LegacyFilter,
SearchViewRowRequest, SearchViewRowRequest,
SearchFilterChild,
} from "@budibase/types" } from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
import merge from "lodash/merge" import merge from "lodash/merge"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { db, roles, features } from "@budibase/backend-core" import { db, roles, features } from "@budibase/backend-core"
import { single } from "validate.js"
describe.each([ describe.each([
["lucene", undefined], ["lucene", undefined],
@ -158,11 +156,11 @@ describe.each([
tableId: table._id!, tableId: table._id!,
primaryDisplay: "id", primaryDisplay: "id",
queryUI: { queryUI: {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.EQUAL, operator: BasicOperator.EQUAL,
@ -2388,10 +2386,10 @@ describe.each([
name: generator.guid(), name: generator.guid(),
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.EQUAL, operator: BasicOperator.EQUAL,
@ -2452,10 +2450,10 @@ describe.each([
name: generator.guid(), name: generator.guid(),
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.EQUAL, operator: BasicOperator.EQUAL,
@ -2741,10 +2739,10 @@ describe.each([
name: generator.guid(), name: generator.guid(),
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.NOT_EQUAL, operator: BasicOperator.NOT_EQUAL,
@ -2799,10 +2797,10 @@ describe.each([
name: generator.guid(), name: generator.guid(),
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.NOT_EQUAL, operator: BasicOperator.NOT_EQUAL,
@ -2894,10 +2892,10 @@ describe.each([
name: generator.guid(), name: generator.guid(),
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.EQUAL, operator: BasicOperator.EQUAL,
@ -3338,10 +3336,10 @@ describe.each([
type: ViewV2Type.CALCULATION, type: ViewV2Type.CALCULATION,
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.EQUAL, operator: BasicOperator.EQUAL,
@ -3631,10 +3629,10 @@ describe.each([
name: generator.guid(), name: generator.guid(),
queryUI: { queryUI: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
filters: [ filters: [
{ {
operator: BasicOperator.EQUAL, operator: BasicOperator.EQUAL,
@ -3711,52 +3709,31 @@ describe.each([
interface TestCase { interface TestCase {
name: string name: string
query: SearchFilterGroup query: UISearchFilter
insert: Row[] insert: Row[]
expected: Row[] expected: Row[]
searchOpts?: SearchViewRowRequest searchOpts?: SearchViewRowRequest
} }
function defaultQuery( function simpleQuery(...filters: LegacyFilter[]): UISearchFilter {
query: Partial<SearchFilterGroup> return { groups: [{ filters }] }
): SearchFilterGroup {
return {
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
logicalOperator: FilterGroupLogicalOperator.ALL,
groups: [],
...query,
}
}
function defaultGroup(
group: Partial<SearchFilterChild>
): SearchFilterChild {
return {
logicalOperator: FilterGroupLogicalOperator.ALL,
filters: [],
...group,
}
}
function simpleQuery(...filters: LegacyFilter[]): SearchFilterGroup {
return defaultQuery({ groups: [defaultGroup({ filters })] })
} }
const testCases: TestCase[] = [ const testCases: TestCase[] = [
{ {
name: "empty query return all", name: "empty query return all",
insert: [{ string: "foo" }], insert: [{ string: "foo" }],
query: defaultQuery({ query: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
}), },
expected: [{ string: "foo" }], expected: [{ string: "foo" }],
}, },
{ {
name: "empty query return none", name: "empty query return none",
insert: [{ string: "foo" }], insert: [{ string: "foo" }],
query: defaultQuery({ query: {
onEmptyFilter: EmptyFilterOption.RETURN_NONE, onEmptyFilter: EmptyFilterOption.RETURN_NONE,
}), },
expected: [], expected: [],
}, },
{ {

View File

@ -14,8 +14,8 @@ export function ensureQueryUISet(view: ViewV2) {
// We're changing it in the change that this comment is part of to also // We're changing it in the change that this comment is part of to also
// include SearchFilters objects. These are created when we receive an // include SearchFilters objects. These are created when we receive an
// update to a ViewV2 that contains a queryUI and not a query field. We // update to a ViewV2 that contains a queryUI and not a query field. We
// can convert SearchFilterGroup (the type of queryUI) to SearchFilters, // can convert UISearchFilter (the type of queryUI) to SearchFilters,
// but not LegacyFilter[], they are incompatible due to SearchFilterGroup // but not LegacyFilter[], they are incompatible due to UISearchFilter
// and SearchFilters being recursive types. // and SearchFilters being recursive types.
// //
// So despite the type saying that `view.query` is a LegacyFilter[] | // So despite the type saying that `view.query` is a LegacyFilter[] |

View File

@ -19,8 +19,8 @@ import {
RangeOperator, RangeOperator,
LogicalOperator, LogicalOperator,
isLogicalSearchOperator, isLogicalSearchOperator,
SearchFilterGroup, UISearchFilter,
FilterGroupLogicalOperator, UILogicalOperator,
isBasicSearchOperator, isBasicSearchOperator,
isArraySearchOperator, isArraySearchOperator,
isRangeSearchOperator, isRangeSearchOperator,
@ -561,7 +561,7 @@ export const buildQueryLegacy = (
} }
/** /**
* Converts a **SearchFilterGroup** 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.
@ -573,49 +573,41 @@ export const buildQueryLegacy = (
*/ */
export function buildQuery(filter: undefined): undefined export function buildQuery(filter: undefined): undefined
export function buildQuery( export function buildQuery(
filter: SearchFilterGroup | LegacyFilter[] filter: UISearchFilter | LegacyFilter[]
): SearchFilters ): SearchFilters
export function buildQuery( export function buildQuery(
filter?: SearchFilterGroup | LegacyFilter[] filter?: UISearchFilter | LegacyFilter[]
): SearchFilters | undefined { ): SearchFilters | undefined {
if (!filter) { if (!filter) {
return return
} }
let parsedFilter: SearchFilterGroup let parsedFilter: UISearchFilter
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
parsedFilter = processSearchFilters(filter) parsedFilter = processSearchFilters(filter)
} else { } else {
parsedFilter = filter parsedFilter = filter
} }
const operatorMap = { const operator = logicalOperatorFromUI(
[FilterGroupLogicalOperator.ALL]: LogicalOperator.AND, parsedFilter.logicalOperator || UILogicalOperator.ALL
[FilterGroupLogicalOperator.ANY]: LogicalOperator.OR, )
} const groups = parsedFilter.groups || []
const conditions: SearchFilters[] = groups.map(group => {
const filters = group.filters || []
return { [operator]: { conditions: filters.map(x => buildCondition(x)) } }
})
const globalOnEmpty = parsedFilter.onEmptyFilter
? parsedFilter.onEmptyFilter
: null
const globalOperator = operatorMap[parsedFilter.logicalOperator]
const ret = {
...(globalOnEmpty ? { onEmptyFilter: globalOnEmpty } : {}),
[globalOperator]: {
conditions: parsedFilter.groups?.map(group => {
return { return {
[operatorMap[group.logicalOperator]]: { onEmptyFilter: parsedFilter.onEmptyFilter,
conditions: group.filters [operator]: { conditions },
?.map(x => buildCondition(x))
.filter(filter => filter),
},
} }
}),
},
} }
return ret function logicalOperatorFromUI(operator: UILogicalOperator): LogicalOperator {
return operator === UILogicalOperator.ALL
? LogicalOperator.AND
: LogicalOperator.OR
} }
// The frontend can send single values for array fields sometimes, so to handle // The frontend can send single values for array fields sometimes, so to handle

View File

@ -1,13 +1,13 @@
import { import {
LegacyFilter, LegacyFilter,
SearchFilterGroup, UISearchFilter,
FilterGroupLogicalOperator, UILogicalOperator,
SearchFilters, SearchFilters,
BasicOperator, BasicOperator,
ArrayOperator, ArrayOperator,
isLogicalSearchOperator, isLogicalSearchOperator,
EmptyFilterOption, EmptyFilterOption,
SearchFilterChild, SearchFilterGroup,
} from "@budibase/types" } from "@budibase/types"
import * as Constants from "./constants" import * as Constants from "./constants"
import { removeKeyNumbering } from "./filters" import { removeKeyNumbering } from "./filters"
@ -132,18 +132,18 @@ export function isSupportedUserSearch(query: SearchFilters) {
/** /**
* Processes the filter config. Filters are migrated from * Processes the filter config. Filters are migrated from
* SearchFilter[] to SearchFilterGroup * SearchFilter[] to UISearchFilter
* *
* If filters is not an array, the migration is skipped * If filters is not an array, the migration is skipped
* *
* @param {LegacyFilter[] | SearchFilterGroup} filters * @param {LegacyFilter[] | UISearchFilter} filters
*/ */
export const processSearchFilters = ( export const processSearchFilters = (
filters: LegacyFilter[] filters: LegacyFilter[]
): SearchFilterGroup => { ): UISearchFilter => {
// Base search config. // Base search config.
const defaultCfg: SearchFilterGroup = { const defaultCfg: UISearchFilter = {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
onEmptyFilter: EmptyFilterOption.RETURN_ALL, onEmptyFilter: EmptyFilterOption.RETURN_ALL,
groups: [], groups: [],
} }
@ -159,11 +159,11 @@ export const processSearchFilters = (
"formulaType", "formulaType",
] ]
let baseGroup: SearchFilterChild = { let baseGroup: SearchFilterGroup = {
logicalOperator: FilterGroupLogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
} }
return filters.reduce((acc: SearchFilterGroup, filter: LegacyFilter) => { return filters.reduce((acc: UISearchFilter, filter: LegacyFilter) => {
// Sort the properties for easier debugging // Sort the properties for easier debugging
const filterPropertyKeys = (Object.keys(filter) as (keyof LegacyFilter)[]) const filterPropertyKeys = (Object.keys(filter) as (keyof LegacyFilter)[])
.sort((a, b) => { .sort((a, b) => {
@ -180,7 +180,7 @@ export const processSearchFilters = (
acc.onEmptyFilter = value acc.onEmptyFilter = value
} else if (key === "operator" && value === "allOr") { } else if (key === "operator" && value === "allOr") {
// Group 1 logical operator // Group 1 logical operator
baseGroup.logicalOperator = FilterGroupLogicalOperator.ANY baseGroup.logicalOperator = UILogicalOperator.ANY
} }
return acc return acc

View File

@ -1,9 +1,5 @@
import { FieldType } from "../../documents" import { FieldType } from "../../documents"
import { import { EmptyFilterOption, UILogicalOperator, SearchFilters } from "../../sdk"
EmptyFilterOption,
FilterGroupLogicalOperator,
SearchFilters,
} from "../../sdk"
export type LegacyFilter = { export type LegacyFilter = {
operator: keyof SearchFilters | "rangeLow" | "rangeHigh" operator: keyof SearchFilters | "rangeLow" | "rangeHigh"
@ -14,15 +10,15 @@ export type LegacyFilter = {
externalType?: string externalType?: string
} }
export type SearchFilterChild = { export type SearchFilterGroup = {
logicalOperator: FilterGroupLogicalOperator logicalOperator?: UILogicalOperator
groups?: SearchFilterChild[] groups?: SearchFilterGroup[]
filters?: LegacyFilter[] filters?: LegacyFilter[]
} }
// this is a type purely used by the UI // this is a type purely used by the UI
export type SearchFilterGroup = { export type UISearchFilter = {
logicalOperator: FilterGroupLogicalOperator logicalOperator?: UILogicalOperator
onEmptyFilter: EmptyFilterOption onEmptyFilter?: EmptyFilterOption
groups: SearchFilterChild[] groups?: SearchFilterGroup[]
} }

View File

@ -1,4 +1,4 @@
import { LegacyFilter, SearchFilterGroup, SortOrder, SortType } from "../../api" import { LegacyFilter, UISearchFilter, SortOrder, SortType } from "../../api"
import { UIFieldMetadata } from "./table" import { UIFieldMetadata } from "./table"
import { Document } from "../document" import { Document } from "../document"
import { DBView, SearchFilters } from "../../sdk" import { DBView, SearchFilters } from "../../sdk"
@ -92,7 +92,7 @@ export interface ViewV2 {
tableId: string tableId: string
query?: LegacyFilter[] | SearchFilters query?: LegacyFilter[] | SearchFilters
// duplicate to store UI information about filters // duplicate to store UI information about filters
queryUI?: SearchFilterGroup queryUI?: UISearchFilter
sort?: { sort?: {
field: string field: string
order?: SortOrder order?: SortOrder

View File

@ -203,7 +203,7 @@ export enum EmptyFilterOption {
RETURN_NONE = "none", RETURN_NONE = "none",
} }
export enum FilterGroupLogicalOperator { export enum UILogicalOperator {
ALL = "all", ALL = "all",
ANY = "any", ANY = "any",
} }