budibase/packages/server/src/sdk/app/rows/search/sqs.ts

205 lines
5.4 KiB
TypeScript
Raw Normal View History

import {
FieldType,
Operation,
QueryJson,
RelationshipFieldMetadata,
Row,
2024-03-26 14:52:22 +01:00
SearchFilters,
RowSearchParams,
2024-03-26 17:34:19 +01:00
SearchResponse,
2024-03-26 14:52:22 +01:00
SortDirection,
SortOrder,
SortType,
Table,
} from "@budibase/types"
import SqlQueryBuilder from "../../../../integrations/base/sql"
import { SqlClient } from "../../../../integrations/utils"
2024-03-26 14:52:22 +01:00
import {
buildInternalRelationships,
sqlOutputProcessing,
} from "../../../../api/controllers/row/utils"
import sdk from "../../../index"
import { context, SQLITE_DESIGN_DOC_ID } from "@budibase/backend-core"
2024-04-12 17:15:36 +02:00
import {
CONSTANT_INTERNAL_ROW_COLS,
SQS_DATASOURCE_INTERNAL,
} from "../../../../db/utils"
import AliasTables from "../sqlAlias"
import { outputProcessing } from "../../../../utilities/rowProcessor"
function buildInternalFieldList(
table: Table,
tables: Table[],
opts: { relationships: boolean } = { relationships: true }
) {
let fieldList: string[] = []
fieldList = fieldList.concat(
CONSTANT_INTERNAL_ROW_COLS.map(col => `${table._id}.${col}`)
)
2024-04-12 17:15:36 +02:00
for (let col of Object.values(table.schema)) {
const isRelationship = col.type === FieldType.LINK
if (!opts.relationships && isRelationship) {
continue
}
if (isRelationship) {
const linkCol = col as RelationshipFieldMetadata
const relatedTable = tables.find(table => table._id === linkCol.tableId)!
fieldList = fieldList.concat(
buildInternalFieldList(relatedTable, tables, { relationships: false })
)
} else {
fieldList.push(`${table._id}.${col.name}`)
}
}
return fieldList
}
function tableInFilter(name: string) {
return `:${name}.`
}
function cleanupFilters(filters: SearchFilters, tables: Table[]) {
for (let filter of Object.values(filters)) {
if (typeof filter !== "object") {
continue
}
for (let [key, keyFilter] of Object.entries(filter)) {
if (keyFilter === "") {
delete filter[key]
}
// relationship, switch to table ID
const tableRelated = tables.find(
table =>
table.originalName && key.includes(tableInFilter(table.originalName))
)
if (tableRelated && tableRelated.originalName) {
filter[
key.replace(
tableInFilter(tableRelated.originalName),
tableInFilter(tableRelated._id!)
)
] = filter[key]
delete filter[key]
}
}
}
return filters
}
function buildTableMap(tables: Table[]) {
const tableMap: Record<string, Table> = {}
for (let table of tables) {
// update the table name, should never query by name for SQLite
table.originalName = table.name
table.name = table._id!
tableMap[table._id!] = table
}
return tableMap
}
export async function search(
2024-04-12 17:15:36 +02:00
options: RowSearchParams,
table: Table
): Promise<SearchResponse<Row>> {
2024-04-12 17:15:36 +02:00
const { paginate, query, ...params } = options
const builder = new SqlQueryBuilder(SqlClient.SQL_LITE)
const allTables = await sdk.tables.getAllInternalTables()
const allTablesMap = buildTableMap(allTables)
if (!table) {
throw new Error("Unable to find table")
}
const relationships = buildInternalRelationships(table)
const request: QueryJson = {
endpoint: {
// not important, we query ourselves
2024-04-12 17:15:36 +02:00
datasourceId: SQS_DATASOURCE_INTERNAL,
entityId: table._id!,
operation: Operation.READ,
},
filters: cleanupFilters(query, allTables),
table,
meta: {
table,
tables: allTablesMap,
},
resource: {
fields: buildInternalFieldList(table, allTables),
},
relationships,
}
if (params.sort) {
const sortField = table.schema[params.sort]
2024-03-26 14:52:22 +01:00
const sortType =
sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING
const sortDirection =
params.sortOrder === SortOrder.ASCENDING
? SortDirection.ASCENDING
: SortDirection.DESCENDING
request.sort = {
[sortField.name]: {
direction: sortDirection,
type: sortType as SortType,
},
}
}
2024-03-26 14:52:22 +01:00
if (paginate && params.limit) {
request.paginate = {
limit: params.limit,
page: params.bookmark,
}
}
try {
2024-04-12 17:15:36 +02:00
const alias = new AliasTables(allTables.map(table => table.name))
const rows = await alias.queryWithAliasing(request, async json => {
const query = builder._query(json, {
disableReturning: true,
})
2024-04-12 17:15:36 +02:00
if (Array.isArray(query)) {
throw new Error("SQS cannot currently handle multiple queries")
}
2024-04-04 19:16:23 +02:00
2024-05-03 18:29:20 +02:00
let sql = query.sql
let bindings = query.bindings
2024-04-04 19:16:23 +02:00
2024-04-12 17:15:36 +02:00
// quick hack for docIds
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")
sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`")
2024-04-12 17:15:36 +02:00
const db = context.getAppDB()
return await db.sql<Row>(sql, bindings)
})
// process from the format of tableId.column to expected format
const processed = await sqlOutputProcessing(
rows,
table!,
allTablesMap,
relationships,
{
sqs: true,
}
)
return {
2024-04-12 17:15:36 +02:00
// final row processing for response
rows: await outputProcessing<Row[]>(table, processed, {
preserveLinks: true,
squash: true,
}),
}
} catch (err: any) {
const msg = typeof err === "string" ? err : err.message
if (err.status === 404 && err.message?.includes(SQLITE_DESIGN_DOC_ID)) {
await sdk.tables.sqs.syncDefinition()
return search(options, table)
}
2024-04-16 18:28:21 +02:00
throw new Error(`Unable to search by SQL - ${msg}`, { cause: err })
}
}