Merge branch 'master' into backport-v3-view-updates
This commit is contained in:
commit
1972ed6533
|
@ -1,3 +1,3 @@
|
|||
nodejs 20.10.0
|
||||
python 3.10.0
|
||||
yarn 1.22.19
|
||||
yarn 1.22.22
|
||||
|
|
|
@ -3,17 +3,11 @@ import {
|
|||
ViewV2,
|
||||
SearchRowResponse,
|
||||
SearchViewRowRequest,
|
||||
SearchFilterKey,
|
||||
LogicalOperator,
|
||||
RequiredKeys,
|
||||
RowSearchParams,
|
||||
LegacyFilter,
|
||||
} from "@budibase/types"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { db, context, features } from "@budibase/backend-core"
|
||||
import { enrichSearchContext } from "./utils"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
import { context } from "@budibase/backend-core"
|
||||
|
||||
export async function searchView(
|
||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||
|
@ -33,65 +27,15 @@ export async function searchView(
|
|||
.map(([key]) => key)
|
||||
const { body } = ctx.request
|
||||
|
||||
const sqsEnabled = await features.flags.isEnabled("SQS")
|
||||
const supportsLogicalOperators = isExternalTableID(view.tableId) || sqsEnabled
|
||||
|
||||
// Enrich saved query with ephemeral query params.
|
||||
// We prevent searching on any fields that are saved as part of the query, as
|
||||
// that could let users find rows they should not be allowed to access.
|
||||
let query = dataFilters.buildQueryLegacy(view.query)
|
||||
|
||||
delete query?.onEmptyFilter
|
||||
|
||||
if (body.query) {
|
||||
// Delete extraneous search params that cannot be overridden
|
||||
delete body.query.onEmptyFilter
|
||||
|
||||
if (!supportsLogicalOperators) {
|
||||
// In the unlikely event that a Grouped Filter is in a non-SQS environment
|
||||
// It needs to be ignored entirely
|
||||
let queryFilters: LegacyFilter[] = Array.isArray(view.query)
|
||||
? view.query
|
||||
: []
|
||||
|
||||
// Extract existing fields
|
||||
const existingFields =
|
||||
queryFilters
|
||||
?.filter(filter => filter.field)
|
||||
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
||||
|
||||
// Carry over filters for unused fields
|
||||
Object.keys(body.query).forEach(key => {
|
||||
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
||||
Object.keys(body.query[operator] || {}).forEach(field => {
|
||||
if (query && !existingFields.includes(db.removeKeyNumbering(field))) {
|
||||
query[operator]![field] = body.query[operator]![field]
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const conditions = query ? [query] : []
|
||||
query = {
|
||||
$and: {
|
||||
conditions: [...conditions, body.query],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await context.ensureSnippetContext(true)
|
||||
|
||||
const enrichedQuery = await enrichSearchContext(query || {}, {
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
})
|
||||
|
||||
const searchOptions: RequiredKeys<SearchViewRowRequest> &
|
||||
RequiredKeys<
|
||||
Pick<RowSearchParams, "tableId" | "viewId" | "query" | "fields">
|
||||
> = {
|
||||
tableId: view.tableId,
|
||||
viewId: view.id,
|
||||
query: enrichedQuery,
|
||||
query: body.query,
|
||||
fields: viewFields,
|
||||
...getSortOptions(body, view),
|
||||
limit: body.limit,
|
||||
|
@ -100,7 +44,9 @@ export async function searchView(
|
|||
countRows: body.countRows,
|
||||
}
|
||||
|
||||
const result = await sdk.rows.search(searchOptions)
|
||||
const result = await sdk.rows.search(searchOptions, {
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
})
|
||||
result.rows.forEach(r => (r._viewId = view.id))
|
||||
ctx.body = result
|
||||
}
|
||||
|
|
|
@ -1738,6 +1738,40 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
it("views filters are respected even if the column is hidden", 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: false },
|
||||
},
|
||||
})
|
||||
|
||||
const response = await config.api.viewV2.search(view.id)
|
||||
expect(response.rows).toHaveLength(1)
|
||||
expect(response.rows).toEqual([
|
||||
expect.objectContaining({ _id: two._id }),
|
||||
])
|
||||
})
|
||||
|
||||
it("views without data can be returned", async () => {
|
||||
const response = await config.api.viewV2.search(view.id)
|
||||
expect(response.rows).toHaveLength(0)
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import {
|
||||
EmptyFilterOption,
|
||||
LegacyFilter,
|
||||
LogicalOperator,
|
||||
Row,
|
||||
RowSearchParams,
|
||||
SearchFilterKey,
|
||||
SearchResponse,
|
||||
SortOrder,
|
||||
Table,
|
||||
|
@ -14,9 +17,10 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
|||
import { dataFilters } from "@budibase/shared-core"
|
||||
import sdk from "../../index"
|
||||
import { searchInputMapping } from "./search/utils"
|
||||
import { features } from "@budibase/backend-core"
|
||||
import { db, features } from "@budibase/backend-core"
|
||||
import tracer from "dd-trace"
|
||||
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||
import { enrichSearchContext } from "../../../api/controllers/row/utils"
|
||||
|
||||
export { isValidFilter } from "../../../integrations/utils"
|
||||
|
||||
|
@ -34,7 +38,8 @@ function pickApi(tableId: any) {
|
|||
}
|
||||
|
||||
export async function search(
|
||||
options: RowSearchParams
|
||||
options: RowSearchParams,
|
||||
context?: Record<string, any>
|
||||
): Promise<SearchResponse<Row>> {
|
||||
return await tracer.trace("search", async span => {
|
||||
span?.addTags({
|
||||
|
@ -51,7 +56,86 @@ export async function search(
|
|||
countRows: options.countRows,
|
||||
})
|
||||
|
||||
options.query = dataFilters.cleanupQuery(options.query || {})
|
||||
let source: Table | ViewV2
|
||||
let table: Table
|
||||
if (options.viewId) {
|
||||
source = await sdk.views.get(options.viewId)
|
||||
table = await sdk.views.getTable(source)
|
||||
} else if (options.tableId) {
|
||||
source = await sdk.tables.getTable(options.tableId)
|
||||
table = source
|
||||
} else {
|
||||
throw new Error(`Must supply either a view ID or a table ID`)
|
||||
}
|
||||
|
||||
const isExternalTable = isExternalTableID(table._id!)
|
||||
|
||||
if (options.query) {
|
||||
const visibleFields = (
|
||||
options.fields || Object.keys(table.schema)
|
||||
).filter(field => table.schema[field]?.visible !== false)
|
||||
|
||||
const queryableFields = await getQueryableFields(table, visibleFields)
|
||||
options.query = removeInvalidFilters(options.query, queryableFields)
|
||||
} else {
|
||||
options.query = {}
|
||||
}
|
||||
|
||||
if (options.viewId) {
|
||||
// Delete extraneous search params that cannot be overridden
|
||||
delete options.query.onEmptyFilter
|
||||
|
||||
options = searchInputMapping(table, options)
|
||||
|
||||
const view = source as ViewV2
|
||||
// Enrich saved query with ephemeral query params.
|
||||
// We prevent searching on any fields that are saved as part of the query, as
|
||||
// that could let users find rows they should not be allowed to access.
|
||||
let viewQuery = dataFilters.buildQueryLegacy(view.query || [])
|
||||
delete viewQuery?.onEmptyFilter
|
||||
|
||||
const sqsEnabled = await features.flags.isEnabled("SQS")
|
||||
const supportsLogicalOperators =
|
||||
isExternalTableID(view.tableId) || sqsEnabled
|
||||
if (!supportsLogicalOperators) {
|
||||
// In the unlikely event that a Grouped Filter is in a non-SQS environment
|
||||
// It needs to be ignored entirely
|
||||
let queryFilters: LegacyFilter[] = Array.isArray(view.query)
|
||||
? view.query
|
||||
: []
|
||||
|
||||
// Extract existing fields
|
||||
const existingFields =
|
||||
queryFilters
|
||||
?.filter(filter => filter.field)
|
||||
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
||||
|
||||
viewQuery ??= {}
|
||||
// Carry over filters for unused fields
|
||||
Object.keys(options.query).forEach(key => {
|
||||
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
||||
Object.keys(options.query[operator] || {}).forEach(field => {
|
||||
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
||||
viewQuery![operator]![field] = options.query[operator]![field]
|
||||
}
|
||||
})
|
||||
})
|
||||
options.query = viewQuery
|
||||
} else {
|
||||
const conditions = viewQuery ? [viewQuery] : []
|
||||
options.query = {
|
||||
$and: {
|
||||
conditions: [...conditions, options.query],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context) {
|
||||
options.query = await enrichSearchContext(options.query, context)
|
||||
}
|
||||
|
||||
options.query = dataFilters.cleanupQuery(options.query)
|
||||
options.query = dataFilters.fixupFilterArrays(options.query)
|
||||
|
||||
span.addTags({
|
||||
|
@ -72,30 +156,6 @@ export async function search(
|
|||
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
|
||||
}
|
||||
|
||||
let source: Table | ViewV2
|
||||
let table: Table
|
||||
if (options.viewId) {
|
||||
source = await sdk.views.get(options.viewId)
|
||||
table = await sdk.views.getTable(source)
|
||||
options = searchInputMapping(table, options)
|
||||
} else if (options.tableId) {
|
||||
source = await sdk.tables.getTable(options.tableId)
|
||||
table = source
|
||||
options = searchInputMapping(table, options)
|
||||
} else {
|
||||
throw new Error(`Must supply either a view ID or a table ID`)
|
||||
}
|
||||
|
||||
if (options.query) {
|
||||
const visibleFields = (
|
||||
options.fields || Object.keys(table.schema)
|
||||
).filter(field => table.schema[field]?.visible !== false)
|
||||
|
||||
const queryableFields = await getQueryableFields(table, visibleFields)
|
||||
options.query = removeInvalidFilters(options.query, queryableFields)
|
||||
}
|
||||
|
||||
const isExternalTable = isExternalTableID(table._id!)
|
||||
let result: SearchResponse<Row>
|
||||
if (isExternalTable) {
|
||||
span?.addTags({ searchType: "external" })
|
||||
|
|
|
@ -130,6 +130,26 @@ export function getUserContextBindings(user: ContextUser) {
|
|||
return {}
|
||||
}
|
||||
// Current user context for bindable search
|
||||
const { _id, _rev, firstName, lastName, email, status, roleId } = user
|
||||
return { _id, _rev, firstName, lastName, email, status, roleId }
|
||||
const {
|
||||
_id,
|
||||
_rev,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
status,
|
||||
roleId,
|
||||
globalId,
|
||||
userId,
|
||||
} = user
|
||||
return {
|
||||
_id,
|
||||
_rev,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
status,
|
||||
roleId,
|
||||
globalId,
|
||||
userId,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue