diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index 851c5b39c9..937e3b6c69 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -41,7 +41,7 @@ { label: "False", value: "false" }, ]} /> -{:else if schema.type === "array"} +{:else if schemaHasOptions(schema) && schema.type === "array"} onChange(e, field)} useLabel={false} /> -{:else if ["string", "number", "bigint", "barcodeqr"].includes(schema.type)} +{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)} parseFloat(item)) + } else { + searchParam[property] = parseFloat(value) + } } } } diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index 5e24b640d4..46d765a7b5 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -143,100 +143,104 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { oneOf: {}, containsAny: {}, } - if (Array.isArray(filter)) { - filter.forEach(expression => { - let { operator, field, type, value, externalType, onEmptyFilter } = - expression - const isHbs = - typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0 - // Parse all values into correct types - if (operator === "allOr") { - query.allOr = true + + if (!Array.isArray(filter)) { + return query + } + + filter.forEach(expression => { + let { operator, field, type, value, externalType, onEmptyFilter } = + expression + const isHbs = + typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0 + // Parse all values into correct types + if (operator === "allOr") { + query.allOr = true + return + } + if (onEmptyFilter) { + query.onEmptyFilter = onEmptyFilter + return + } + if ( + type === "datetime" && + !isHbs && + operator !== "empty" && + operator !== "notEmpty" + ) { + // Ensure date value is a valid date and parse into correct format + if (!value) { return } - if (onEmptyFilter) { - query.onEmptyFilter = onEmptyFilter + try { + value = new Date(value).toISOString() + } catch (error) { return } - if ( - type === "datetime" && - !isHbs && - operator !== "empty" && - operator !== "notEmpty" + } + if (type === "number" && typeof value === "string" && !isHbs) { + if (operator === "oneOf") { + value = value.split(",").map(item => parseFloat(item)) + } else { + value = parseFloat(value) + } + } + if (type === "boolean") { + value = `${value}`?.toLowerCase() === "true" + } + if ( + ["contains", "notContains", "containsAny"].includes(operator) && + type === "array" && + typeof value === "string" + ) { + value = value.split(",") + } + if (operator.startsWith("range") && query.range) { + const minint = + SqlNumberTypeRangeMap[ + externalType as keyof typeof SqlNumberTypeRangeMap + ]?.min || Number.MIN_SAFE_INTEGER + const maxint = + SqlNumberTypeRangeMap[ + externalType as keyof typeof SqlNumberTypeRangeMap + ]?.max || Number.MAX_SAFE_INTEGER + if (!query.range[field]) { + query.range[field] = { + low: type === "number" ? minint : "0000-00-00T00:00:00.000Z", + high: type === "number" ? maxint : "9999-00-00T00:00:00.000Z", + } + } + if ((operator as any) === "rangeLow" && value != null && value !== "") { + query.range[field].low = value + } else if ( + (operator as any) === "rangeHigh" && + value != null && + value !== "" ) { - // Ensure date value is a valid date and parse into correct format - if (!value) { - return - } - try { - value = new Date(value).toISOString() - } catch (error) { - return - } - } - if (type === "number" && typeof value === "string") { - if (operator === "oneOf") { - value = value.split(",").map(item => parseFloat(item)) - } else if (!isHbs) { - value = parseFloat(value) - } + query.range[field].high = value } + } else if (query[operator] && operator !== "onEmptyFilter") { if (type === "boolean") { - value = `${value}`?.toLowerCase() === "true" - } - if ( - ["contains", "notContains", "containsAny"].includes(operator) && - type === "array" && - typeof value === "string" - ) { - value = value.split(",") - } - if (operator.startsWith("range") && query.range) { - const minint = - SqlNumberTypeRangeMap[ - externalType as keyof typeof SqlNumberTypeRangeMap - ]?.min || Number.MIN_SAFE_INTEGER - const maxint = - SqlNumberTypeRangeMap[ - externalType as keyof typeof SqlNumberTypeRangeMap - ]?.max || Number.MAX_SAFE_INTEGER - if (!query.range[field]) { - query.range[field] = { - low: type === "number" ? minint : "0000-00-00T00:00:00.000Z", - high: type === "number" ? maxint : "9999-00-00T00:00:00.000Z", - } - } - if ((operator as any) === "rangeLow" && value != null && value !== "") { - query.range[field].low = value - } else if ( - (operator as any) === "rangeHigh" && - value != null && - value !== "" - ) { - query.range[field].high = value - } - } else if (query[operator] && 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) { - query.notEqual = query.notEqual || {} - query.notEqual[field] = true - } else if (operator === "notEqual" && value === false) { - query.equal = query.equal || {} - query.equal[field] = true - } else { - query[operator] = query[operator] || {} - query[operator]![field] = value - } + // 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) { + query.notEqual = query.notEqual || {} + query.notEqual[field] = true + } else if (operator === "notEqual" && value === false) { + query.equal = query.equal || {} + query.equal[field] = true } else { query[operator] = query[operator] || {} query[operator]![field] = value } + } else { + query[operator] = query[operator] || {} + query[operator]![field] = value } - }) - } + } + }) + return query } diff --git a/packages/shared-core/src/tests/filters.test.ts b/packages/shared-core/src/tests/filters.test.ts index bddd6cb1f0..8586d58777 100644 --- a/packages/shared-core/src/tests/filters.test.ts +++ b/packages/shared-core/src/tests/filters.test.ts @@ -1,6 +1,11 @@ -import { SearchQuery, SearchQueryOperators } from "@budibase/types" -import { runLuceneQuery } from "../filters" -import { expect, describe, it } from "vitest" +import { + SearchQuery, + SearchQueryOperators, + FieldType, + SearchFilter, +} from "@budibase/types" +import { buildLuceneQuery, runLuceneQuery } from "../filters" +import { expect, describe, it, test } from "vitest" describe("runLuceneQuery", () => { const docs = [ @@ -167,4 +172,186 @@ describe("runLuceneQuery", () => { }) expect(runLuceneQuery(docs, query).map(row => row.order_id)).toEqual([2, 3]) }) + + test.each([[523, 259], "523,259"])( + "should return rows with matches on numeric oneOf filter", + input => { + let query = buildQuery("oneOf", { + customer_id: input, + }) + expect(runLuceneQuery(docs, query).map(row => row.customer_id)).toEqual([ + 259, 523, + ]) + } + ) +}) + +describe("buildLuceneQuery", () => { + it("should return a basic search query template if the input is not an array", () => { + const filter: any = "NOT_AN_ARRAY" + expect(buildLuceneQuery(filter)).toEqual({ + string: {}, + fuzzy: {}, + range: {}, + equal: {}, + notEqual: {}, + empty: {}, + notEmpty: {}, + contains: {}, + notContains: {}, + oneOf: {}, + containsAny: {}, + }) + }) + + it("should parseFloat if the type is a number, but the value is a numeric string", () => { + const filter: SearchFilter[] = [ + { + operator: SearchQueryOperators.EQUAL, + field: "customer_id", + type: FieldType.NUMBER, + value: "1212", + }, + { + operator: SearchQueryOperators.ONE_OF, + field: "customer_id", + type: FieldType.NUMBER, + value: "1000,1212,3400", + }, + ] + expect(buildLuceneQuery(filter)).toEqual({ + string: {}, + fuzzy: {}, + range: {}, + equal: { + customer_id: 1212, + }, + notEqual: {}, + empty: {}, + notEmpty: {}, + contains: {}, + notContains: {}, + oneOf: { + customer_id: [1000, 1212, 3400], + }, + containsAny: {}, + }) + }) + + it("should not parseFloat if the type is a number, but the value is a handlebars binding string", () => { + const filter: SearchFilter[] = [ + { + operator: SearchQueryOperators.EQUAL, + field: "customer_id", + type: FieldType.NUMBER, + value: "{{ customer_id }}", + }, + { + operator: SearchQueryOperators.ONE_OF, + field: "customer_id", + type: FieldType.NUMBER, + value: "{{ list_of_customer_ids }}", + }, + ] + expect(buildLuceneQuery(filter)).toEqual({ + string: {}, + fuzzy: {}, + range: {}, + equal: { + customer_id: "{{ customer_id }}", + }, + notEqual: {}, + empty: {}, + notEmpty: {}, + contains: {}, + notContains: {}, + oneOf: { + customer_id: "{{ list_of_customer_ids }}", + }, + containsAny: {}, + }) + }) + + it("should cast string to boolean if the type is boolean", () => { + const filter: SearchFilter[] = [ + { + operator: SearchQueryOperators.EQUAL, + field: "a", + type: FieldType.BOOLEAN, + value: "not_true", + }, + { + operator: SearchQueryOperators.NOT_EQUAL, + field: "b", + type: FieldType.BOOLEAN, + value: "not_true", + }, + { + operator: SearchQueryOperators.EQUAL, + field: "c", + type: FieldType.BOOLEAN, + value: "true", + }, + ] + expect(buildLuceneQuery(filter)).toEqual({ + string: {}, + fuzzy: {}, + range: {}, + equal: { + b: true, + c: true, + }, + notEqual: { + a: true, + }, + empty: {}, + notEmpty: {}, + contains: {}, + notContains: {}, + oneOf: {}, + containsAny: {}, + }) + }) + + it("should split the string for contains operators", () => { + const filter: SearchFilter[] = [ + { + operator: SearchQueryOperators.CONTAINS, + field: "description", + type: FieldType.ARRAY, + value: "Large box,Heavy box,Small box", + }, + { + operator: SearchQueryOperators.NOT_CONTAINS, + field: "description", + type: FieldType.ARRAY, + value: "Large box,Heavy box,Small box", + }, + { + operator: SearchQueryOperators.CONTAINS_ANY, + field: "description", + type: FieldType.ARRAY, + value: "Large box,Heavy box,Small box", + }, + ] + expect(buildLuceneQuery(filter)).toEqual({ + string: {}, + fuzzy: {}, + range: {}, + equal: {}, + notEqual: {}, + empty: {}, + notEmpty: {}, + contains: { + description: ["Large box", "Heavy box", "Small box"], + }, + notContains: { + description: ["Large box", "Heavy box", "Small box"], + }, + oneOf: {}, + containsAny: { + description: ["Large box", "Heavy box", "Small box"], + }, + }) + }) })