Move query logic to sdk

This commit is contained in:
Adria Navarro 2024-09-30 15:18:42 +02:00
parent 26638ace0a
commit 6e1cd6eb01
2 changed files with 87 additions and 80 deletions

View File

@ -3,14 +3,9 @@ import {
ViewV2, ViewV2,
SearchRowResponse, SearchRowResponse,
SearchViewRowRequest, SearchViewRowRequest,
SearchFilterKey,
LogicalOperator,
} from "@budibase/types" } from "@budibase/types"
import { dataFilters } from "@budibase/shared-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { db, context, features } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
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>
@ -27,58 +22,23 @@ export async function searchView(
const { body } = ctx.request const { body } = ctx.request
// 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.buildQuery(view.query || [])
if (body.query) {
// Delete extraneous search params that cannot be overridden
delete body.query.onEmptyFilter
if (
!isExternalTableID(view.tableId) &&
!(await features.flags.isEnabled("SQS"))
) {
// Extract existing fields
const existingFields =
view.query
?.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 (!existingFields.includes(db.removeKeyNumbering(field))) {
query[operator]![field] = body.query[operator]![field]
}
})
})
} else {
query = {
$and: {
conditions: [query, body.query],
},
}
}
}
await context.ensureSnippetContext(true) await context.ensureSnippetContext(true)
const enrichedQuery = await enrichSearchContext(query, { const result = await sdk.rows.search(
user: sdk.users.getUserContextBindings(ctx.user), {
}) viewId: view.id,
tableId: view.tableId,
const result = await sdk.rows.search({ query: body.query,
viewId: view.id, ...getSortOptions(body, view),
tableId: view.tableId, limit: body.limit,
query: enrichedQuery, bookmark: body.bookmark,
...getSortOptions(body, view), paginate: body.paginate,
limit: body.limit, countRows: body.countRows,
bookmark: body.bookmark, },
paginate: body.paginate, {
countRows: body.countRows, user: sdk.users.getUserContextBindings(ctx.user),
}) }
)
result.rows.forEach(r => (r._viewId = view.id)) result.rows.forEach(r => (r._viewId = view.id))
ctx.body = result ctx.body = result

View File

@ -1,7 +1,10 @@
import { import {
EmptyFilterOption, EmptyFilterOption,
LogicalOperator,
Row, Row,
RowSearchParams, RowSearchParams,
SearchFilterKey,
SearchFilters,
SearchResponse, SearchResponse,
SortOrder, SortOrder,
Table, Table,
@ -14,9 +17,10 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import sdk from "../../index" import sdk from "../../index"
import { searchInputMapping } from "./search/utils" import { searchInputMapping } from "./search/utils"
import { features } from "@budibase/backend-core" import { db, features } from "@budibase/backend-core"
import tracer from "dd-trace" import tracer from "dd-trace"
import { getQueryableFields, removeInvalidFilters } from "./queryUtils" import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
import { enrichSearchContext } from "../../../api/controllers/row/utils"
export { isValidFilter } from "../../../integrations/utils" export { isValidFilter } from "../../../integrations/utils"
@ -34,7 +38,8 @@ function pickApi(tableId: any) {
} }
export async function search( export async function search(
options: RowSearchParams options: RowSearchParams,
context?: Record<string, any>
): Promise<SearchResponse<Row>> { ): Promise<SearchResponse<Row>> {
return await tracer.trace("search", async span => { return await tracer.trace("search", async span => {
span?.addTags({ span?.addTags({
@ -51,6 +56,69 @@ export async function search(
countRows: options.countRows, countRows: options.countRows,
}) })
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
} 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)
}
if (options.viewId) {
const view = await sdk.views.get(options.viewId)
// 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.buildQuery(view.query || [])
if (!isExternalTable && !(await features.flags.isEnabled("SQS"))) {
// Lucene does not accept conditional filters, so we need to keep the old logic
const query: SearchFilters = {}
// Extract existing fields
const existingFields =
view.query
?.filter(filter => filter.field)
.map(filter => db.removeKeyNumbering(filter.field)) || []
// 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))) {
query[operator]![field] = viewQuery[operator]![field]
} else {
query[operator]![field] = options.query[operator]![field]
}
})
})
} else {
options.query = {
$and: {
conditions: [viewQuery, options.query],
},
}
}
}
if (context) {
options.query = await enrichSearchContext(options.query, context)
}
options.query = dataFilters.cleanupQuery(options.query || {}) options.query = dataFilters.cleanupQuery(options.query || {})
options.query = dataFilters.fixupFilterArrays(options.query) options.query = dataFilters.fixupFilterArrays(options.query)
@ -72,28 +140,7 @@ export async function search(
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
} }
let source: Table | ViewV2 options = searchInputMapping(table, options)
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!) const isExternalTable = isExternalTableID(table._id!)
let result: SearchResponse<Row> let result: SearchResponse<Row>