diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 48920a3771..7d62a6ef39 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -17,8 +17,8 @@ import { ContextUser, CouchFindOptions, DatabaseQueryOpts, - SearchQuery, - SearchQueryOperators, + SearchFilters, + SearchFilterOperator, SearchUsersRequest, User, } from "@budibase/types" @@ -44,11 +44,11 @@ function removeUserPassword(users: User | User[]) { return users } -export function isSupportedUserSearch(query: SearchQuery) { +export function isSupportedUserSearch(query: SearchFilters) { const allowed = [ - { op: SearchQueryOperators.STRING, key: "email" }, - { op: SearchQueryOperators.EQUAL, key: "_id" }, - { op: SearchQueryOperators.ONE_OF, key: "_id" }, + { op: SearchFilterOperator.STRING, key: "email" }, + { op: SearchFilterOperator.EQUAL, key: "_id" }, + { op: SearchFilterOperator.ONE_OF, key: "_id" }, ] for (let [key, operation] of Object.entries(query)) { if (typeof operation !== "object") { diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index 45b81f2a78..1b252d5b06 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -11,7 +11,7 @@ Label, Multiselect, } from "@budibase/bbui" - import { FieldType, SearchQueryOperators } from "@budibase/types" + import { FieldType, SearchFilterOperator } from "@budibase/types" import { generate } from "shortid" import { LuceneUtils, Constants } from "@budibase/frontend-core" import { getContext } from "svelte" @@ -247,7 +247,7 @@ {:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)} - {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)} { const response = await search.paginatedSearch( { contains: { - column: "a", + column: ["a"], colArr: [1, 2, 3], }, }, @@ -168,7 +168,7 @@ describe("internal search", () => { ) checkLucene( response, - `(*:* AND column:a AND colArr:(1 AND 2 AND 3))`, + `(*:* AND column:(a) AND colArr:(1 AND 2 AND 3))`, PARAMS ) }) diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index 4416b28b08..6010f064bf 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -4,9 +4,9 @@ import { FieldType, FormulaType, SearchFilter, - SearchQuery, + SearchFilters, SearchQueryFields, - SearchQueryOperators, + SearchFilterOperator, SortDirection, SortType, } from "@budibase/types" @@ -99,18 +99,19 @@ export const NoEmptyFilterStrings = [ * Removes any fields that contain empty strings that would cause inconsistent * behaviour with how backend tables are filtered (no value means no filter). */ -const cleanupQuery = (query: SearchQuery) => { +const cleanupQuery = (query: SearchFilters) => { if (!query) { return query } for (let filterField of NoEmptyFilterStrings) { - if (!query[filterField]) { + const operator = filterField as SearchFilterOperator + if (!query[operator]) { continue } - for (let [key, value] of Object.entries(query[filterField]!)) { + for (let [key, value] of Object.entries(query[operator]!)) { if (value == null || value === "") { - delete query[filterField]![key] + delete query[operator]![key] } } } @@ -136,7 +137,7 @@ export const removeKeyNumbering = (key: string): string => { * @param filter the builder filter structure */ export const buildLuceneQuery = (filter: SearchFilter[]) => { - let query: SearchQuery = { + let query: SearchFilters = { string: {}, fuzzy: {}, range: {}, @@ -157,6 +158,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { filter.forEach(expression => { let { operator, field, type, value, externalType, onEmptyFilter } = expression + const queryOperator = operator as SearchFilterOperator const isHbs = typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0 // Parse all values into correct types @@ -171,8 +173,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { if ( type === "datetime" && !isHbs && - operator !== "empty" && - operator !== "notEmpty" + queryOperator !== "empty" && + queryOperator !== "notEmpty" ) { // Ensure date value is a valid date and parse into correct format if (!value) { @@ -185,7 +187,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { } } if (type === "number" && typeof value === "string" && !isHbs) { - if (operator === "oneOf") { + if (queryOperator === "oneOf") { value = value.split(",").map(item => parseFloat(item)) } else { value = parseFloat(value) @@ -225,24 +227,24 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { ) { query.range[field].high = value } - } else if (query[operator] && operator !== "onEmptyFilter") { + } else if (query[queryOperator] && operator !== "onEmptyFilter") { if (type === "boolean") { // Transform boolean filters to cope with null. // "equals false" needs to be "not equals true" // "not equals false" needs to be "equals true" - if (operator === "equal" && value === false) { + if (queryOperator === "equal" && value === false) { query.notEqual = query.notEqual || {} query.notEqual[field] = true - } else if (operator === "notEqual" && value === false) { + } else if (queryOperator === "notEqual" && value === false) { query.equal = query.equal || {} query.equal[field] = true } else { - query[operator] = query[operator] || {} - query[operator]![field] = value + query[queryOperator] = query[queryOperator] || {} + query[queryOperator]![field] = value } } else { - query[operator] = query[operator] || {} - query[operator]![field] = value + query[queryOperator] = query[queryOperator] || {} + query[queryOperator]![field] = value } } }) @@ -255,7 +257,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { * @param docs the data * @param query the JSON lucene query */ -export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { +export const runLuceneQuery = (docs: any[], query?: SearchFilters) => { if (!docs || !Array.isArray(docs)) { return [] } @@ -269,7 +271,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Iterates over a set of filters and evaluates a fail function against a doc const match = ( - type: keyof SearchQueryFields, + type: SearchFilterOperator, failFn: (docValue: any, testValue: any) => boolean ) => (doc: any) => { @@ -286,7 +288,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a string match (fails if the value does not start with the string) const stringMatch = match( - SearchQueryOperators.STRING, + SearchFilterOperator.STRING, (docValue: string, testValue: string) => { return ( !docValue || @@ -297,7 +299,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a fuzzy match (treat the same as starts with when running locally) const fuzzyMatch = match( - SearchQueryOperators.FUZZY, + SearchFilterOperator.FUZZY, (docValue: string, testValue: string) => { return ( !docValue || @@ -308,7 +310,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a range match const rangeMatch = match( - SearchQueryOperators.RANGE, + SearchFilterOperator.RANGE, ( docValue: string | number | null, testValue: { low: number; high: number } @@ -331,7 +333,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process an equal match (fails if the value is different) const equalMatch = match( - SearchQueryOperators.EQUAL, + SearchFilterOperator.EQUAL, (docValue: any, testValue: string | null) => { return testValue != null && testValue !== "" && docValue !== testValue } @@ -339,7 +341,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a not-equal match (fails if the value is the same) const notEqualMatch = match( - SearchQueryOperators.NOT_EQUAL, + SearchFilterOperator.NOT_EQUAL, (docValue: any, testValue: string | null) => { return testValue != null && testValue !== "" && docValue === testValue } @@ -347,7 +349,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process an empty match (fails if the value is not empty) const emptyMatch = match( - SearchQueryOperators.EMPTY, + SearchFilterOperator.EMPTY, (docValue: string | null) => { return docValue != null && docValue !== "" } @@ -355,7 +357,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a not-empty match (fails is the value is empty) const notEmptyMatch = match( - SearchQueryOperators.NOT_EMPTY, + SearchFilterOperator.NOT_EMPTY, (docValue: string | null) => { return docValue == null || docValue === "" } @@ -363,7 +365,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process an includes match (fails if the value is not included) const oneOf = match( - SearchQueryOperators.ONE_OF, + SearchFilterOperator.ONE_OF, (docValue: any, testValue: any) => { if (typeof testValue === "string") { testValue = testValue.split(",") @@ -376,28 +378,28 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { ) const containsAny = match( - SearchQueryOperators.CONTAINS_ANY, + SearchFilterOperator.CONTAINS_ANY, (docValue: any, testValue: any) => { return !docValue?.includes(...testValue) } ) const contains = match( - SearchQueryOperators.CONTAINS, + SearchFilterOperator.CONTAINS, (docValue: string | any[], testValue: any[]) => { return !testValue?.every((item: any) => docValue?.includes(item)) } ) const notContains = match( - SearchQueryOperators.NOT_CONTAINS, + SearchFilterOperator.NOT_CONTAINS, (docValue: string | any[], testValue: any[]) => { return testValue?.every((item: any) => docValue?.includes(item)) } ) const docMatch = (doc: any) => { - const filterFunctions: Record boolean> = + const filterFunctions: Record boolean> = { string: stringMatch, fuzzy: fuzzyMatch, @@ -412,7 +414,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { notContains: notContains, } - const activeFilterKeys: SearchQueryOperators[] = Object.entries(query || {}) + const activeFilterKeys: SearchFilterOperator[] = Object.entries(query || {}) .filter( ([key, value]: [string, any]) => !["allOr", "onEmptyFilter"].includes(key) && @@ -480,7 +482,7 @@ export const luceneLimit = (docs: any[], limit: string) => { return docs.slice(0, numLimit) } -export const hasFilters = (query?: SearchQuery) => { +export const hasFilters = (query?: SearchFilters) => { if (!query) { return false } diff --git a/packages/shared-core/src/tests/filters.test.ts b/packages/shared-core/src/tests/filters.test.ts index e74e37d681..f188c5f951 100644 --- a/packages/shared-core/src/tests/filters.test.ts +++ b/packages/shared-core/src/tests/filters.test.ts @@ -1,6 +1,6 @@ import { - SearchQuery, - SearchQueryOperators, + SearchFilters, + SearchFilterOperator, FieldType, SearchFilter, } from "@budibase/types" @@ -46,8 +46,8 @@ describe("runLuceneQuery", () => { }, ] - function buildQuery(filters: { [filterKey: string]: any }): SearchQuery { - const query: SearchQuery = { + function buildQuery(filters: { [filterKey: string]: any }): SearchFilters { + const query: SearchFilters = { string: {}, fuzzy: {}, range: {}, @@ -63,7 +63,7 @@ describe("runLuceneQuery", () => { } for (const filterKey in filters) { - query[filterKey as SearchQueryOperators] = filters[filterKey] + query[filterKey as SearchFilterOperator] = filters[filterKey] } return query @@ -265,13 +265,13 @@ describe("buildLuceneQuery", () => { it("should parseFloat if the type is a number, but the value is a numeric string", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "customer_id", type: FieldType.NUMBER, value: "1212", }, { - operator: SearchQueryOperators.ONE_OF, + operator: SearchFilterOperator.ONE_OF, field: "customer_id", type: FieldType.NUMBER, value: "1000,1212,3400", @@ -299,13 +299,13 @@ describe("buildLuceneQuery", () => { it("should not parseFloat if the type is a number, but the value is a handlebars binding string", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "customer_id", type: FieldType.NUMBER, value: "{{ customer_id }}", }, { - operator: SearchQueryOperators.ONE_OF, + operator: SearchFilterOperator.ONE_OF, field: "customer_id", type: FieldType.NUMBER, value: "{{ list_of_customer_ids }}", @@ -333,19 +333,19 @@ describe("buildLuceneQuery", () => { it("should cast string to boolean if the type is boolean", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "a", type: FieldType.BOOLEAN, value: "not_true", }, { - operator: SearchQueryOperators.NOT_EQUAL, + operator: SearchFilterOperator.NOT_EQUAL, field: "b", type: FieldType.BOOLEAN, value: "not_true", }, { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "c", type: FieldType.BOOLEAN, value: "true", @@ -374,19 +374,19 @@ describe("buildLuceneQuery", () => { it("should split the string for contains operators", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.CONTAINS, + operator: SearchFilterOperator.CONTAINS, field: "description", type: FieldType.ARRAY, value: "Large box,Heavy box,Small box", }, { - operator: SearchQueryOperators.NOT_CONTAINS, + operator: SearchFilterOperator.NOT_CONTAINS, field: "description", type: FieldType.ARRAY, value: "Large box,Heavy box,Small box", }, { - operator: SearchQueryOperators.CONTAINS_ANY, + operator: SearchFilterOperator.CONTAINS_ANY, field: "description", type: FieldType.ARRAY, value: "Large box,Heavy box,Small box", diff --git a/packages/types/src/api/web/searchFilter.ts b/packages/types/src/api/web/searchFilter.ts index ac3c446e36..5223204a7f 100644 --- a/packages/types/src/api/web/searchFilter.ts +++ b/packages/types/src/api/web/searchFilter.ts @@ -1,68 +1,11 @@ import { FieldType } from "../../documents" -import { EmptyFilterOption } from "../../sdk" +import { EmptyFilterOption, SearchFilters } from "../../sdk" export type SearchFilter = { - operator: keyof SearchQuery + operator: keyof SearchFilters | "rangeLow" | "rangeHigh" onEmptyFilter?: EmptyFilterOption field: string type?: FieldType value: any externalType?: string } - -export enum SearchQueryOperators { - STRING = "string", - FUZZY = "fuzzy", - RANGE = "range", - EQUAL = "equal", - NOT_EQUAL = "notEqual", - EMPTY = "empty", - NOT_EMPTY = "notEmpty", - ONE_OF = "oneOf", - CONTAINS = "contains", - NOT_CONTAINS = "notContains", - CONTAINS_ANY = "containsAny", -} - -export type SearchQuery = { - allOr?: boolean - onEmptyFilter?: EmptyFilterOption - [SearchQueryOperators.STRING]?: { - [key: string]: string - } - [SearchQueryOperators.FUZZY]?: { - [key: string]: string - } - [SearchQueryOperators.RANGE]?: { - [key: string]: { - high: number | string - low: number | string - } - } - [SearchQueryOperators.EQUAL]?: { - [key: string]: any - } - [SearchQueryOperators.NOT_EQUAL]?: { - [key: string]: any - } - [SearchQueryOperators.EMPTY]?: { - [key: string]: any - } - [SearchQueryOperators.NOT_EMPTY]?: { - [key: string]: any - } - [SearchQueryOperators.ONE_OF]?: { - [key: string]: any[] - } - [SearchQueryOperators.CONTAINS]?: { - [key: string]: any[] - } - [SearchQueryOperators.NOT_CONTAINS]?: { - [key: string]: any[] - } - [SearchQueryOperators.CONTAINS_ANY]?: { - [key: string]: any[] - } -} - -export type SearchQueryFields = Omit diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 0ef7493016..10630c272c 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -1,5 +1,5 @@ import { User } from "../../documents" -import { SearchQuery } from "./searchFilter" +import { SearchFilters } from "../../sdk" export interface SaveUserResponse { _id: string @@ -55,7 +55,7 @@ export interface InviteUsersResponse { export interface SearchUsersRequest { bookmark?: string - query?: SearchQuery + query?: SearchFilters appId?: string limit?: number paginate?: boolean diff --git a/packages/types/src/sdk/search.ts b/packages/types/src/sdk/search.ts index 0b93fb9215..51d866c9de 100644 --- a/packages/types/src/sdk/search.ts +++ b/packages/types/src/sdk/search.ts @@ -3,47 +3,63 @@ import { Row, Table } from "../documents" import { SortType } from "../api" import { Knex } from "knex" +export enum SearchFilterOperator { + STRING = "string", + FUZZY = "fuzzy", + RANGE = "range", + EQUAL = "equal", + NOT_EQUAL = "notEqual", + EMPTY = "empty", + NOT_EMPTY = "notEmpty", + ONE_OF = "oneOf", + CONTAINS = "contains", + NOT_CONTAINS = "notContains", + CONTAINS_ANY = "containsAny", +} + export interface SearchFilters { allOr?: boolean onEmptyFilter?: EmptyFilterOption - string?: { + [SearchFilterOperator.STRING]?: { [key: string]: string } - fuzzy?: { + [SearchFilterOperator.FUZZY]?: { [key: string]: string } - range?: { + [SearchFilterOperator.RANGE]?: { [key: string]: { high: number | string low: number | string } } - equal?: { + [SearchFilterOperator.EQUAL]?: { [key: string]: any } - notEqual?: { + [SearchFilterOperator.NOT_EQUAL]?: { [key: string]: any } - empty?: { + [SearchFilterOperator.EMPTY]?: { [key: string]: any } - notEmpty?: { + [SearchFilterOperator.NOT_EMPTY]?: { [key: string]: any } - oneOf?: { + [SearchFilterOperator.ONE_OF]?: { [key: string]: any[] } - contains?: { - [key: string]: any[] | any - } - notContains?: { + [SearchFilterOperator.CONTAINS]?: { [key: string]: any[] } - containsAny?: { + [SearchFilterOperator.NOT_CONTAINS]?: { + [key: string]: any[] + } + [SearchFilterOperator.CONTAINS_ANY]?: { [key: string]: any[] } } +export type SearchQueryFields = Omit + export interface SortJson { [key: string]: { direction: SortDirection diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index d08a4ef8c7..541004391d 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -4,7 +4,7 @@ import { InviteUsersRequest, User, CreateAdminUserRequest, - SearchQuery, + SearchFilters, InviteUsersResponse, } from "@budibase/types" import structures from "../structures" @@ -150,7 +150,7 @@ export class UserAPI extends TestAPI { } searchUsers = ( - { query }: { query?: SearchQuery }, + { query }: { query?: SearchFilters }, opts?: { status?: number; noHeaders?: boolean } ) => { const req = this.request