1
0
Fork 0
mirror of synced 2024-08-18 19:41:30 +12:00

Merge pull request #9998 from Budibase/fix/budi-6720-dev

Match any filter + not contains fix
This commit is contained in:
Michael Drury 2023-03-13 12:11:31 +00:00 committed by GitHub
commit a5a8a68587
2 changed files with 78 additions and 15 deletions

View file

@ -199,6 +199,10 @@ export class QueryBuilder<T> {
return this return this
} }
setAllOr() {
this.query.allOr = true
}
handleSpaces(input: string) { handleSpaces(input: string) {
if (this.noEscaping) { if (this.noEscaping) {
return input return input
@ -236,6 +240,36 @@ export class QueryBuilder<T> {
return value return value
} }
isMultiCondition() {
let count = 0
for (let filters of Object.values(this.query)) {
// not contains is one massive filter in allOr mode
if (typeof filters === "object") {
count += Object.keys(filters).length
}
}
return count > 1
}
compressFilters(filters: Record<string, string[]>) {
const compressed: typeof filters = {}
for (let key of Object.keys(filters)) {
const finalKey = removeKeyNumbering(key)
if (compressed[finalKey]) {
compressed[finalKey] = compressed[finalKey].concat(filters[key])
} else {
compressed[finalKey] = filters[key]
}
}
// add prefixes back
const final: typeof filters = {}
let count = 1
for (let [key, value] of Object.entries(compressed)) {
final[`${count++}:${key}`] = value
}
return final
}
buildSearchQuery() { buildSearchQuery() {
const builder = this const builder = this
let allOr = this.query && this.query.allOr let allOr = this.query && this.query.allOr
@ -272,9 +306,9 @@ export class QueryBuilder<T> {
} }
const notContains = (key: string, value: any) => { const notContains = (key: string, value: any) => {
// @ts-ignore const allPrefix = allOr ? "*:* AND " : ""
const allPrefix = allOr === "" ? "*:* AND" : "" const mode = allOr ? "AND" : undefined
return allPrefix + "NOT " + contains(key, value) return allPrefix + "NOT " + contains(key, value, mode)
} }
const containsAny = (key: string, value: any) => { const containsAny = (key: string, value: any) => {
@ -299,21 +333,32 @@ export class QueryBuilder<T> {
return `${key}:(${orStatement})` return `${key}:(${orStatement})`
} }
function build(structure: any, queryFn: any) { function build(
structure: any,
queryFn: (key: string, value: any) => string | null,
opts?: { returnBuilt?: boolean; mode?: string }
) {
let built = ""
for (let [key, value] of Object.entries(structure)) { for (let [key, value] of Object.entries(structure)) {
// check for new format - remove numbering if needed // check for new format - remove numbering if needed
key = removeKeyNumbering(key) key = removeKeyNumbering(key)
key = builder.preprocess(builder.handleSpaces(key), { key = builder.preprocess(builder.handleSpaces(key), {
escape: true, escape: true,
}) })
const expression = queryFn(key, value) let expression = queryFn(key, value)
if (expression == null) { if (expression == null) {
continue continue
} }
if (query.length > 0) { if (built.length > 0 || query.length > 0) {
query += ` ${allOr ? "OR" : "AND"} ` const mode = opts?.mode ? opts.mode : allOr ? "OR" : "AND"
built += ` ${mode} `
} }
query += expression built += expression
}
if (opts?.returnBuilt) {
return built
} else {
query += built
} }
} }
@ -384,14 +429,14 @@ export class QueryBuilder<T> {
build(this.query.contains, contains) build(this.query.contains, contains)
} }
if (this.query.notContains) { if (this.query.notContains) {
build(this.query.notContains, notContains) build(this.compressFilters(this.query.notContains), notContains)
} }
if (this.query.containsAny) { if (this.query.containsAny) {
build(this.query.containsAny, containsAny) build(this.query.containsAny, containsAny)
} }
// make sure table ID is always added as an AND // make sure table ID is always added as an AND
if (tableId) { if (tableId) {
query = `(${query})` query = this.isMultiCondition() ? `(${query})` : query
allOr = false allOr = false
build({ tableId }, equal) build({ tableId }, equal)
} }

View file

@ -6,9 +6,13 @@ import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
const INDEX_NAME = "main" const INDEX_NAME = "main"
const index = `function(doc) { const index = `function(doc) {
let props = ["property", "number"] let props = ["property", "number", "array"]
for (let key of props) { for (let key of props) {
if (doc[key]) { if (Array.isArray(doc[key])) {
for (let val of doc[key]) {
index(key, val)
}
} else if (doc[key]) {
index(key, doc[key]) index(key, doc[key])
} }
} }
@ -21,9 +25,14 @@ describe("lucene", () => {
dbName = `db-${newid()}` dbName = `db-${newid()}`
// create the DB for testing // create the DB for testing
db = getDB(dbName) db = getDB(dbName)
await db.put({ _id: newid(), property: "word" }) await db.put({ _id: newid(), property: "word", array: ["1", "4"] })
await db.put({ _id: newid(), property: "word2" }) await db.put({ _id: newid(), property: "word2", array: ["3", "1"] })
await db.put({ _id: newid(), property: "word3", number: 1 }) await db.put({
_id: newid(),
property: "word3",
number: 1,
array: ["1", "2"],
})
}) })
it("should be able to create a lucene index", async () => { it("should be able to create a lucene index", async () => {
@ -118,6 +127,15 @@ describe("lucene", () => {
const resp = await builder.run() const resp = await builder.run()
expect(resp.rows.length).toBe(2) expect(resp.rows.length).toBe(2)
}) })
it("should be able to perform an or not contains search", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addNotContains("array", ["1"])
builder.addNotContains("array", ["2"])
builder.setAllOr()
const resp = await builder.run()
expect(resp.rows.length).toBe(2)
})
}) })
describe("paginated search", () => { describe("paginated search", () => {