Merge pull request #14335 from Budibase/BUDI-8508/conditions-on-views
Support filtering views
This commit is contained in:
commit
dac3fa0675
|
@ -12,6 +12,7 @@ import { dataFilters } from "@budibase/shared-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { db, context } from "@budibase/backend-core"
|
import { db, context } from "@budibase/backend-core"
|
||||||
import { enrichSearchContext } from "./utils"
|
import { enrichSearchContext } from "./utils"
|
||||||
|
import { isExternalTableID } from "../../../integrations/utils"
|
||||||
|
|
||||||
export async function searchView(
|
export async function searchView(
|
||||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||||
|
@ -36,33 +37,33 @@ export async function searchView(
|
||||||
// that could let users find rows they should not be allowed to access.
|
// that could let users find rows they should not be allowed to access.
|
||||||
let query = dataFilters.buildQuery(view.query || [])
|
let query = dataFilters.buildQuery(view.query || [])
|
||||||
if (body.query) {
|
if (body.query) {
|
||||||
// Extract existing fields
|
|
||||||
const existingFields =
|
|
||||||
view.query
|
|
||||||
?.filter(filter => filter.field)
|
|
||||||
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
|
||||||
|
|
||||||
// Delete extraneous search params that cannot be overridden
|
// Delete extraneous search params that cannot be overridden
|
||||||
delete body.query.allOr
|
delete body.query.allOr
|
||||||
delete body.query.onEmptyFilter
|
delete body.query.onEmptyFilter
|
||||||
|
|
||||||
// Carry over filters for unused fields
|
if (!isExternalTableID(view.tableId) && !db.isSqsEnabledForTenant()) {
|
||||||
Object.keys(body.query).forEach(key => {
|
// Extract existing fields
|
||||||
const operator = key as SearchFilterKey
|
const existingFields =
|
||||||
|
view.query
|
||||||
|
?.filter(filter => filter.field)
|
||||||
|
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
||||||
|
|
||||||
Object.keys(body.query[operator] || {}).forEach(field => {
|
// Carry over filters for unused fields
|
||||||
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
Object.keys(body.query).forEach(key => {
|
||||||
if (
|
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
||||||
operator === LogicalOperator.AND ||
|
Object.keys(body.query[operator] || {}).forEach(field => {
|
||||||
operator === LogicalOperator.OR
|
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
||||||
) {
|
|
||||||
// TODO
|
|
||||||
} else {
|
|
||||||
query[operator]![field] = body.query[operator]![field]
|
query[operator]![field] = body.query[operator]![field]
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
})
|
} else {
|
||||||
|
query = {
|
||||||
|
$and: {
|
||||||
|
conditions: [query, body.query],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.ensureSnippetContext(true)
|
await context.ensureSnippetContext(true)
|
||||||
|
|
|
@ -1485,6 +1485,119 @@ describe.each([
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isLucene &&
|
||||||
|
it("in lucene, cannot override a view filter", async () => {
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
one: "foo",
|
||||||
|
two: "bar",
|
||||||
|
})
|
||||||
|
const two = await config.api.row.save(table._id!, {
|
||||||
|
one: "foo2",
|
||||||
|
two: "bar2",
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
query: [
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "two",
|
||||||
|
value: "bar2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: false },
|
||||||
|
two: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
two: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(response.rows).toHaveLength(1)
|
||||||
|
expect(response.rows).toEqual([
|
||||||
|
expect.objectContaining({ _id: two._id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
!isLucene &&
|
||||||
|
it("can filter a view without a view filter", async () => {
|
||||||
|
const one = await config.api.row.save(table._id!, {
|
||||||
|
one: "foo",
|
||||||
|
two: "bar",
|
||||||
|
})
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
one: "foo2",
|
||||||
|
two: "bar2",
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: false },
|
||||||
|
two: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
two: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(response.rows).toHaveLength(1)
|
||||||
|
expect(response.rows).toEqual([
|
||||||
|
expect.objectContaining({ _id: one._id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
!isLucene &&
|
||||||
|
it("cannot bypass a view filter", async () => {
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
one: "foo",
|
||||||
|
two: "bar",
|
||||||
|
})
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
one: "foo2",
|
||||||
|
two: "bar2",
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
query: [
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "two",
|
||||||
|
value: "bar2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: false },
|
||||||
|
two: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
two: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(response.rows).toHaveLength(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("permissions", () => {
|
describe("permissions", () => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Datasource,
|
Datasource,
|
||||||
DocumentType,
|
DocumentType,
|
||||||
FieldType,
|
FieldType,
|
||||||
LogicalOperator,
|
isLogicalSearchOperator,
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
RelationshipFieldMetadata,
|
RelationshipFieldMetadata,
|
||||||
|
@ -141,10 +141,7 @@ function cleanupFilters(
|
||||||
|
|
||||||
const prefixFilters = (filters: SearchFilters) => {
|
const prefixFilters = (filters: SearchFilters) => {
|
||||||
for (const filterKey of Object.keys(filters) as (keyof SearchFilters)[]) {
|
for (const filterKey of Object.keys(filters) as (keyof SearchFilters)[]) {
|
||||||
if (
|
if (isLogicalSearchOperator(filterKey)) {
|
||||||
filterKey === LogicalOperator.AND ||
|
|
||||||
filterKey === LogicalOperator.OR
|
|
||||||
) {
|
|
||||||
for (const condition of filters[filterKey]!.conditions) {
|
for (const condition of filters[filterKey]!.conditions) {
|
||||||
prefixFilters(condition)
|
prefixFilters(condition)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
BasicOperator,
|
BasicOperator,
|
||||||
RangeOperator,
|
RangeOperator,
|
||||||
LogicalOperator,
|
LogicalOperator,
|
||||||
|
isLogicalSearchOperator,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||||
|
@ -359,10 +360,7 @@ export const buildQuery = (filter: SearchFilter[]) => {
|
||||||
high: value,
|
high: value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (isLogicalSearchOperator(queryOperator)) {
|
||||||
queryOperator === LogicalOperator.AND ||
|
|
||||||
queryOperator === LogicalOperator.OR
|
|
||||||
) {
|
|
||||||
// TODO
|
// TODO
|
||||||
} else if (query[queryOperator] && operator !== "onEmptyFilter") {
|
} else if (query[queryOperator] && operator !== "onEmptyFilter") {
|
||||||
if (type === "boolean") {
|
if (type === "boolean") {
|
||||||
|
@ -464,10 +462,9 @@ export const runQuery = (docs: Record<string, any>[], query: SearchFilters) => {
|
||||||
) =>
|
) =>
|
||||||
(doc: Record<string, any>) => {
|
(doc: Record<string, any>) => {
|
||||||
for (const [key, testValue] of Object.entries(query[type] || {})) {
|
for (const [key, testValue] of Object.entries(query[type] || {})) {
|
||||||
const valueToCheck =
|
const valueToCheck = isLogicalSearchOperator(type)
|
||||||
type === LogicalOperator.AND || type === LogicalOperator.OR
|
? doc
|
||||||
? doc
|
: deepGet(doc, removeKeyNumbering(key))
|
||||||
: deepGet(doc, removeKeyNumbering(key))
|
|
||||||
const result = test(valueToCheck, testValue)
|
const result = test(valueToCheck, testValue)
|
||||||
if (query.allOr && result) {
|
if (query.allOr && result) {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -28,6 +28,12 @@ export enum LogicalOperator {
|
||||||
OR = "$or",
|
OR = "$or",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLogicalSearchOperator(
|
||||||
|
value: string
|
||||||
|
): value is LogicalOperator {
|
||||||
|
return value === LogicalOperator.AND || value === LogicalOperator.OR
|
||||||
|
}
|
||||||
|
|
||||||
export type SearchFilterOperator =
|
export type SearchFilterOperator =
|
||||||
| BasicOperator
|
| BasicOperator
|
||||||
| ArrayOperator
|
| ArrayOperator
|
||||||
|
|
Loading…
Reference in New Issue