Rejig view calculation code to work with aggregates again. Broke some other tests in the process.

This commit is contained in:
Sam Rose 2024-09-26 10:54:04 +01:00
parent efd677e16a
commit 43265bf1ea
No known key found for this signature in database
9 changed files with 135 additions and 114 deletions

View File

@ -5,7 +5,7 @@ import {
Row, Row,
Table, Table,
JsonTypes, JsonTypes,
Aggregation, ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { import {
helpers, helpers,
@ -13,6 +13,7 @@ import {
PROTECTED_INTERNAL_COLUMNS, PROTECTED_INTERNAL_COLUMNS,
} from "@budibase/shared-core" } from "@budibase/shared-core"
import { generateRowIdField } from "../../../../integrations/utils" import { generateRowIdField } from "../../../../integrations/utils"
import sdk from "../../../../sdk"
function extractFieldValue({ function extractFieldValue({
row, row,
@ -85,22 +86,28 @@ function fixJsonTypes(row: Row, table: Table) {
return row return row
} }
export function basicProcessing({ export async function basicProcessing({
row, row,
table, source,
tables, tables,
isLinked, isLinked,
sqs, sqs,
aggregations,
}: { }: {
row: Row row: Row
table: Table source: Table | ViewV2
tables: Table[] tables: Table[]
isLinked: boolean isLinked: boolean
sqs?: boolean sqs?: boolean
aggregations?: Aggregation[] }): Promise<Row> {
}): Row { let table: Table
if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id)
} else {
table = source
}
const thisRow: Row = {} const thisRow: Row = {}
// filter the row down to what is actually the row (not joined) // filter the row down to what is actually the row (not joined)
for (let fieldName of Object.keys(table.schema)) { for (let fieldName of Object.keys(table.schema)) {
let value = extractFieldValue({ let value = extractFieldValue({
@ -118,8 +125,10 @@ export function basicProcessing({
} }
} }
for (let aggregation of aggregations || []) { if (sdk.views.isView(source)) {
thisRow[aggregation.name] = row[aggregation.name] for (const key of Object.keys(helpers.views.calculationFields(source))) {
thisRow[key] = row[key]
}
} }
let columns: string[] = Object.keys(table.schema) let columns: string[] = Object.keys(table.schema)
@ -163,28 +172,30 @@ export function basicProcessing({
thisRow[col] = array thisRow[col] = array
// make sure all of them have an _id // make sure all of them have an _id
const sortField = relatedTable.primaryDisplay || relatedTable.primary![0]! const sortField = relatedTable.primaryDisplay || relatedTable.primary![0]!
thisRow[col] = (thisRow[col] as Row[]) thisRow[col] = (
.map(relatedRow => await Promise.all(
basicProcessing({ (thisRow[col] as Row[]).map(relatedRow =>
row: relatedRow, basicProcessing({
table: relatedTable, row: relatedRow,
tables, source: relatedTable,
isLinked: false, tables,
sqs, isLinked: false,
}) sqs,
})
)
) )
.sort((a, b) => { ).sort((a, b) => {
const aField = a?.[sortField], const aField = a?.[sortField],
bField = b?.[sortField] bField = b?.[sortField]
if (!aField) { if (!aField) {
return 1 return 1
} else if (!bField) { } else if (!bField) {
return -1 return -1
} }
return aField.localeCompare return aField.localeCompare
? aField.localeCompare(bField) ? aField.localeCompare(bField)
: aField - bField : aField - bField
}) })
} }
} }
return fixJsonTypes(thisRow, table) return fixJsonTypes(thisRow, table)

View File

@ -7,6 +7,7 @@ import {
ManyToManyRelationshipFieldMetadata, ManyToManyRelationshipFieldMetadata,
RelationshipFieldMetadata, RelationshipFieldMetadata,
RelationshipsJson, RelationshipsJson,
Row,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import { breakExternalTableId } from "../../../../integrations/utils" import { breakExternalTableId } from "../../../../integrations/utils"
@ -149,3 +150,7 @@ export function isKnexEmptyReadResponse(resp: DatasourcePlusQueryResponse) {
(DSPlusOperation.READ in resp[0] && resp[0].read === true) (DSPlusOperation.READ in resp[0] && resp[0].read === true)
) )
} }
export function isKnexRows(resp: DatasourcePlusQueryResponse): resp is Row[] {
return !isKnexEmptyReadResponse(resp)
}

View File

@ -2,7 +2,6 @@ import * as utils from "../../../../db/utils"
import { docIds } from "@budibase/backend-core" import { docIds } from "@budibase/backend-core"
import { import {
Aggregation,
Ctx, Ctx,
DatasourcePlusQueryResponse, DatasourcePlusQueryResponse,
FieldType, FieldType,
@ -15,7 +14,7 @@ import {
processDates, processDates,
processFormulas, processFormulas,
} from "../../../../utilities/rowProcessor" } from "../../../../utilities/rowProcessor"
import { isKnexEmptyReadResponse } from "./sqlUtils" import { isKnexRows } from "./sqlUtils"
import { basicProcessing, generateIdForRow, getInternalRowId } from "./basic" import { basicProcessing, generateIdForRow, getInternalRowId } from "./basic"
import sdk from "../../../../sdk" import sdk from "../../../../sdk"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
@ -97,7 +96,7 @@ export async function getTableFromSource(source: Table | ViewV2) {
return source return source
} }
function fixBooleanFields({ row, table }: { row: Row; table: Table }) { function fixBooleanFields(row: Row, table: Table) {
for (let col of Object.values(table.schema)) { for (let col of Object.values(table.schema)) {
if (col.type === FieldType.BOOLEAN) { if (col.type === FieldType.BOOLEAN) {
if (row[col.name] === 1) { if (row[col.name] === 1) {
@ -115,53 +114,40 @@ export async function sqlOutputProcessing(
source: Table | ViewV2, source: Table | ViewV2,
tables: Record<string, Table>, tables: Record<string, Table>,
relationships: RelationshipsJson[], relationships: RelationshipsJson[],
opts?: { sqs?: boolean; aggregations?: Aggregation[] } opts?: { sqs?: boolean }
): Promise<Row[]> { ): Promise<Row[]> {
if (isKnexEmptyReadResponse(rows)) { if (!isKnexRows(rows)) {
return [] return []
} }
let table: Table let table: Table
if (sdk.views.isView(source)) { if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id) table = await sdk.views.getTable(source.id)
} else { } else {
table = source table = source
} }
let finalRows: { [key: string]: Row } = {}
for (let row of rows as Row[]) { let processedRows: Row[] = []
let rowId = row._id for (let row of rows) {
if (opts?.sqs) { if (opts?.sqs) {
rowId = getInternalRowId(row, table) row._id = getInternalRowId(row, table)
row._id = rowId } else if (row._id == null) {
} else if (!rowId) { row._id = generateIdForRow(row, table)
rowId = generateIdForRow(row, table)
row._id = rowId
} }
const thisRow = basicProcessing({
row = await basicProcessing({
row, row,
table, source,
tables: Object.values(tables), tables: Object.values(tables),
isLinked: false, isLinked: false,
sqs: opts?.sqs, sqs: opts?.sqs,
aggregations: opts?.aggregations,
}) })
if (thisRow._id == null) { row = fixBooleanFields(row, table)
throw new Error("Unable to generate row ID for SQL rows") row = await processRelationshipFields(table, tables, row, relationships)
} processedRows.push(row)
finalRows[thisRow._id] = fixBooleanFields({ row: thisRow, table })
} }
// make sure all related rows are correct return processDates(table, processedRows)
let finalRowArray = []
for (let row of Object.values(finalRows)) {
finalRowArray.push(
await processRelationshipFields(table, tables, row, relationships)
)
}
// process some additional types
finalRowArray = processDates(table, finalRowArray)
return finalRowArray
} }
export function isUserMetadataTable(tableId: string) { export function isUserMetadataTable(tableId: string) {

View File

@ -5,9 +5,8 @@ import {
SearchViewRowRequest, SearchViewRowRequest,
SearchFilterKey, SearchFilterKey,
LogicalOperator, LogicalOperator,
Aggregation,
} from "@budibase/types" } from "@budibase/types"
import { dataFilters, helpers } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { db, context, features } from "@budibase/backend-core" import { db, context, features } from "@budibase/backend-core"
import { enrichSearchContext } from "./utils" import { enrichSearchContext } from "./utils"
@ -26,9 +25,6 @@ export async function searchView(
ctx.throw(400, `This method only supports viewsV2`) ctx.throw(400, `This method only supports viewsV2`)
} }
const viewFields = Object.entries(helpers.views.basicFields(view))
.filter(([_, value]) => value.visible)
.map(([key]) => key)
const { body } = ctx.request const { body } = ctx.request
// Enrich saved query with ephemeral query params. // Enrich saved query with ephemeral query params.
@ -73,25 +69,15 @@ export async function searchView(
user: sdk.users.getUserContextBindings(ctx.user), user: sdk.users.getUserContextBindings(ctx.user),
}) })
const aggregations: Aggregation[] = Object.entries(
helpers.views.calculationFields(view)
).map(([name, { field, calculationType }]) => ({
name,
calculationType,
field,
}))
const result = await sdk.rows.search({ const result = await sdk.rows.search({
viewId: view.id, viewId: view.id,
tableId: view.tableId, tableId: view.tableId,
query: enrichedQuery, query: enrichedQuery,
fields: viewFields,
...getSortOptions(body, view), ...getSortOptions(body, view),
limit: body.limit, limit: body.limit,
bookmark: body.bookmark, bookmark: body.bookmark,
paginate: body.paginate, paginate: body.paginate,
countRows: body.countRows, countRows: body.countRows,
aggregations,
}) })
result.rows.forEach(r => (r._viewId = view.id)) result.rows.forEach(r => (r._viewId = view.id))

View File

@ -40,13 +40,13 @@ import {
import sdk from "../../../sdk" import sdk from "../../../sdk"
describe.each([ describe.each([
["lucene", undefined], // ["lucene", undefined],
["sqs", undefined], ["sqs", undefined],
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
[DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], // [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)],
])("/v2/views (%s)", (name, dsProvider) => { ])("/v2/views (%s)", (name, dsProvider) => {
const config = setup.getConfig() const config = setup.getConfig()
const isSqs = name === "sqs" const isSqs = name === "sqs"
@ -1653,7 +1653,7 @@ describe.each([
}) })
describe("search", () => { describe("search", () => {
it("returns empty rows from view when no schema is passed", async () => { it.only("returns empty rows from view when no schema is passed", async () => {
const rows = await Promise.all( const rows = await Promise.all(
Array.from({ length: 10 }, () => config.api.row.save(table._id!, {})) Array.from({ length: 10 }, () => config.api.row.save(table._id!, {}))
) )
@ -2384,7 +2384,7 @@ describe.each([
}) })
}) })
describe.skip("calculations", () => { describe("calculations", () => {
let table: Table let table: Table
let rows: Row[] let rows: Row[]

View File

@ -49,9 +49,6 @@ export async function search(
paginate: options.paginate, paginate: options.paginate,
fields: options.fields, fields: options.fields,
countRows: options.countRows, countRows: options.countRows,
aggregations: options.aggregations
?.map(a => `${a.field}:${a.calculationType}`)
.join(", "),
}) })
options.query = dataFilters.cleanupQuery(options.query || {}) options.query = dataFilters.cleanupQuery(options.query || {})

View File

@ -1,4 +1,5 @@
import { import {
Aggregation,
Datasource, Datasource,
DocumentType, DocumentType,
FieldType, FieldType,
@ -58,11 +59,34 @@ const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
const MISSING_TABLE_REGX = new RegExp(`no such table: .+`) const MISSING_TABLE_REGX = new RegExp(`no such table: .+`)
const DUPLICATE_COLUMN_REGEX = new RegExp(`duplicate column name: .+`) const DUPLICATE_COLUMN_REGEX = new RegExp(`duplicate column name: .+`)
function buildInternalFieldList( async function buildInternalFieldList(
table: Table, source: Table | ViewV2,
tables: Table[], tables: Table[],
opts?: { relationships?: RelationshipsJson[] } opts?: { relationships?: RelationshipsJson[]; allowedFields?: string[] }
) { ) {
const { relationships, allowedFields } = opts || {}
let schemaFields: string[] = []
if (sdk.views.isView(source)) {
schemaFields = Object.keys(helpers.views.basicFields(source)).filter(
key => source.schema?.[key]?.visible !== false
)
} else {
schemaFields = Object.keys(source.schema).filter(
key => source.schema[key].visible !== false
)
}
if (allowedFields) {
schemaFields = schemaFields.filter(field => allowedFields.includes(field))
}
let table: Table
if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id)
} else {
table = source
}
let fieldList: string[] = [] let fieldList: string[] = []
const getJunctionFields = (relatedTable: Table, fields: string[]) => { const getJunctionFields = (relatedTable: Table, fields: string[]) => {
const junctionFields: string[] = [] const junctionFields: string[] = []
@ -73,13 +97,18 @@ function buildInternalFieldList(
}) })
return junctionFields return junctionFields
} }
fieldList = fieldList.concat( if (sdk.tables.isTable(source)) {
PROTECTED_INTERNAL_COLUMNS.map(col => `${table._id}.${col}`) for (const key of PROTECTED_INTERNAL_COLUMNS) {
) if (allowedFields && !allowedFields.includes(key)) {
for (let key of Object.keys(table.schema)) { continue
}
fieldList.push(`${table._id}.${key}`)
}
}
for (let key of schemaFields) {
const col = table.schema[key] const col = table.schema[key]
const isRelationship = col.type === FieldType.LINK const isRelationship = col.type === FieldType.LINK
if (!opts?.relationships && isRelationship) { if (!relationships && isRelationship) {
continue continue
} }
if (!isRelationship) { if (!isRelationship) {
@ -90,7 +119,9 @@ function buildInternalFieldList(
if (!relatedTable) { if (!relatedTable) {
continue continue
} }
const relatedFields = buildInternalFieldList(relatedTable, tables).concat( const relatedFields = (
await buildInternalFieldList(relatedTable, tables)
).concat(
getJunctionFields(relatedTable, ["doc1.fieldName", "doc2.fieldName"]) getJunctionFields(relatedTable, ["doc1.fieldName", "doc2.fieldName"])
) )
// break out of the loop if we have reached the max number of columns // break out of the loop if we have reached the max number of columns
@ -330,11 +361,20 @@ export async function search(
documentType: DocumentType.ROW, documentType: DocumentType.ROW,
} }
if (options.aggregations) { let aggregations: Aggregation[] = []
options.aggregations = options.aggregations.map(a => { if (sdk.views.isView(source)) {
a.field = mapToUserColumn(a.field) const calculationFields = helpers.views.calculationFields(source)
return a for (const [key, field] of Object.entries(calculationFields)) {
}) if (options.fields && !options.fields.includes(key)) {
continue
}
aggregations.push({
name: key,
field: mapToUserColumn(field.field),
calculationType: field.calculationType,
})
}
} }
const request: QueryJson = { const request: QueryJson = {
@ -352,8 +392,11 @@ export async function search(
columnPrefix: USER_COLUMN_PREFIX, columnPrefix: USER_COLUMN_PREFIX,
}, },
resource: { resource: {
fields: buildInternalFieldList(table, allTables, { relationships }), fields: await buildInternalFieldList(source, allTables, {
aggregations: options.aggregations, relationships,
allowedFields: options.fields,
}),
aggregations,
}, },
relationships, relationships,
} }
@ -400,7 +443,6 @@ export async function search(
table, table,
await sqlOutputProcessing(rows, source, allTablesMap, relationships, { await sqlOutputProcessing(rows, source, allTablesMap, relationships, {
sqs: true, sqs: true,
aggregations: options.aggregations,
}) })
) )
@ -418,16 +460,12 @@ export async function search(
let finalRows = await outputProcessing(source, processed, { let finalRows = await outputProcessing(source, processed, {
preserveLinks: true, preserveLinks: true,
squash: true, squash: true,
aggregations: options.aggregations, aggregations,
}) })
// check if we need to pick specific rows out // check if we need to pick specific rows out
if (options.fields) { if (options.fields) {
const fields = [ const fields = [...options.fields, ...PROTECTED_INTERNAL_COLUMNS]
...options.fields,
...PROTECTED_INTERNAL_COLUMNS,
...(options.aggregations || []).map(a => a.name),
]
finalRows = finalRows.map((r: any) => pick(r, fields)) finalRows = finalRows.map((r: any) => pick(r, fields))
} }
@ -450,7 +488,7 @@ export async function search(
const msg = typeof err === "string" ? err : err.message const msg = typeof err === "string" ? err : err.message
if (!opts?.retrying && resyncDefinitionsRequired(err.status, msg)) { if (!opts?.retrying && resyncDefinitionsRequired(err.status, msg)) {
await sdk.tables.sqs.syncDefinition() await sdk.tables.sqs.syncDefinition()
return search(options, table, { retrying: true }) return search(options, source, { retrying: true })
} }
// previously the internal table didn't error when a column didn't exist in search // previously the internal table didn't error when a column didn't exist in search
if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) { if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) {

View File

@ -26,7 +26,6 @@ export interface SearchViewRowRequest
| "paginate" | "paginate"
| "query" | "query"
| "countRows" | "countRows"
| "aggregations"
> {} > {}
export interface SearchRowResponse { export interface SearchRowResponse {

View File

@ -25,7 +25,6 @@ export interface SearchParams {
indexer?: () => Promise<any> indexer?: () => Promise<any>
rows?: Row[] rows?: Row[]
countRows?: boolean countRows?: boolean
aggregations?: Aggregation[]
} }
// when searching for rows we want a more extensive search type that requires certain properties // when searching for rows we want a more extensive search type that requires certain properties