1
0
Fork 0
mirror of synced 2024-09-25 13:51:40 +12:00

Type checks pass, now to find out how much stuff I've broken.

This commit is contained in:
Sam Rose 2024-09-24 16:35:53 +01:00
parent 0fee7dfd4f
commit 0eb90cfbea
No known key found for this signature in database
24 changed files with 183 additions and 188 deletions

View file

@ -79,8 +79,8 @@ export class QueryBuilder<T> {
return this return this
} }
setTable(tableId: string) { setSource(sourceId: string) {
this.#query.equal!.tableId = tableId this.#query.equal!.tableId = sourceId
return this return this
} }
@ -638,8 +638,8 @@ async function recursiveSearch<T>(
.setSortOrder(params.sortOrder) .setSortOrder(params.sortOrder)
.setSortType(params.sortType) .setSortType(params.sortType)
if (params.tableId) { if (params.sourceId) {
queryBuilder.setTable(params.tableId) queryBuilder.setSource(params.sourceId)
} }
const page = await queryBuilder.run() const page = await queryBuilder.run()
@ -672,8 +672,8 @@ export async function paginatedSearch<T>(
if (params.version) { if (params.version) {
search.setVersion(params.version) search.setVersion(params.version)
} }
if (params.tableId) { if (params.sourceId) {
search.setTable(params.tableId) search.setSource(params.sourceId)
} }
if (params.sort) { if (params.sort) {
search search
@ -695,8 +695,8 @@ export async function paginatedSearch<T>(
// Try fetching 1 row in the next page to see if another page of results // Try fetching 1 row in the next page to see if another page of results
// exists or not // exists or not
search.setBookmark(searchResults.bookmark).setLimit(1) search.setBookmark(searchResults.bookmark).setLimit(1)
if (params.tableId) { if (params.sourceId) {
search.setTable(params.tableId) search.setSource(params.sourceId)
} }
const nextResults = await search.run() const nextResults = await search.run()

View file

@ -366,7 +366,7 @@ describe("lucene", () => {
}, },
}, },
{ {
tableId: TABLE_ID, sourceId: TABLE_ID,
limit: 1, limit: 1,
sort: "property", sort: "property",
sortType: SortType.STRING, sortType: SortType.STRING,
@ -390,7 +390,7 @@ describe("lucene", () => {
}, },
}, },
{ {
tableId: TABLE_ID, sourceId: TABLE_ID,
query: {}, query: {},
} }
) )

View file

@ -45,7 +45,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const table = await utils.getTableFromSource(source) const table = await utils.getTableFromSource(source)
const { _id, ...rowData } = ctx.request.body const { _id, ...rowData } = ctx.request.body
const { row: dataToUpdate } = await inputProcessing( const dataToUpdate = await inputProcessing(
ctx.user?._id, ctx.user?._id,
cloneDeep(source), cloneDeep(source),
rowData rowData

View file

@ -21,18 +21,19 @@ import {
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { getLinkedTableIDs } from "../../../db/linkedRows/linkUtils" import { getLinkedTableIDs } from "../../../db/linkedRows/linkUtils"
import { flatten } from "lodash" import { flatten } from "lodash"
import { findRow } from "../../../sdk/app/rows/internal"
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) { export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const { tableId, viewId } = utils.getSourceId(ctx) const { tableId } = utils.getSourceId(ctx)
const source = await utils.getSource(ctx)
const table = sdk.views.isView(source)
? await sdk.views.getTable(source.id)
: source
const inputs = ctx.request.body const inputs = ctx.request.body
const isUserTable = tableId === InternalTables.USER_METADATA const isUserTable = tableId === InternalTables.USER_METADATA
let oldRow let oldRow
const dbTable = await sdk.tables.getTable(tableId)
try { try {
oldRow = await outputProcessing( oldRow = await outputProcessing(source, await findRow(tableId, inputs._id!))
dbTable,
await utils.findRow(tableId, inputs._id!)
)
} catch (err) { } catch (err) {
if (isUserTable) { if (isUserTable) {
// don't include the rev, it'll be the global rev // don't include the rev, it'll be the global rev
@ -48,22 +49,18 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
// need to build up full patch fields before coerce // need to build up full patch fields before coerce
let combinedRow: any = cloneDeep(oldRow) let combinedRow: any = cloneDeep(oldRow)
for (let key of Object.keys(inputs)) { for (let key of Object.keys(inputs)) {
if (!dbTable.schema[key]) continue if (!table.schema[key]) continue
combinedRow[key] = inputs[key] combinedRow[key] = inputs[key]
} }
// need to copy the table so it can be differenced on way out // need to copy the table so it can be differenced on way out
const tableClone = cloneDeep(dbTable) const tableClone = cloneDeep(table)
// this returns the table and row incase they have been updated // this returns the table and row incase they have been updated
let { table, row } = await inputProcessing( let row = await inputProcessing(ctx.user?._id, tableClone, combinedRow)
ctx.user?._id,
tableClone,
combinedRow
)
const validateResult = await sdk.rows.utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
table, source,
}) })
if (!validateResult.valid) { if (!validateResult.valid) {
@ -87,10 +84,8 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
return { row: ctx.body as Row, table, oldRow } return { row: ctx.body as Row, table, oldRow }
} }
const result = await finaliseRow(table, row, { const result = await finaliseRow(source, row, {
oldTable: dbTable,
updateFormula: true, updateFormula: true,
fromViewId: viewId,
}) })
return { ...result, oldRow } return { ...result, oldRow }
@ -186,7 +181,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
sdk.tables.getTable(tableId), sdk.tables.getTable(tableId),
linkRows.getLinkDocuments({ tableId, rowId, fieldName }), linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
]) ])
let row = await utils.findRow(tableId, rowId) let row = await findRow(tableId, rowId)
row = await outputProcessing(table, row) row = await outputProcessing(table, row)
const linkVals = links as LinkDocumentValue[] const linkVals = links as LinkDocumentValue[]

View file

@ -4,10 +4,11 @@ import {
processFormulas, processFormulas,
} from "../../../utilities/rowProcessor" } from "../../../utilities/rowProcessor"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { Table, Row, FormulaType, FieldType } from "@budibase/types" import { Table, Row, FormulaType, FieldType, ViewV2 } from "@budibase/types"
import * as linkRows from "../../../db/linkedRows" import * as linkRows from "../../../db/linkedRows"
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import sdk from "../../../sdk"
/** /**
* This function runs through a list of enriched rows, looks at the rows which * This function runs through a list of enriched rows, looks at the rows which
@ -121,33 +122,26 @@ export async function updateAllFormulasInTable(table: Table) {
* expects the row to be totally enriched/contain all relationships. * expects the row to be totally enriched/contain all relationships.
*/ */
export async function finaliseRow( export async function finaliseRow(
table: Table, source: Table | ViewV2,
row: Row, row: Row,
{ opts?: { updateFormula: boolean }
oldTable,
updateFormula,
fromViewId,
}: { oldTable?: Table; updateFormula: boolean; fromViewId?: string } = {
updateFormula: true,
}
) { ) {
const db = context.getAppDB() const db = context.getAppDB()
const { updateFormula = true } = opts || {}
const table = sdk.views.isView(source)
? await sdk.views.getTable(source.id)
: source
row.type = "row" row.type = "row"
// process the row before return, to include relationships // process the row before return, to include relationships
let enrichedRow = (await outputProcessing(table, cloneDeep(row), { let enrichedRow = await outputProcessing(source, cloneDeep(row), {
squash: false, squash: false,
})) as Row })
// use enriched row to generate formulas for saving, specifically only use as context // use enriched row to generate formulas for saving, specifically only use as context
row = await processFormulas(table, row, { row = await processFormulas(table, row, {
dynamic: false, dynamic: false,
contextRows: [enrichedRow], contextRows: [enrichedRow],
}) })
// don't worry about rev, tables handle rev/lastID updates
// if another row has been written since processing this will
// handle the auto ID clash
if (oldTable && !isEqual(oldTable, table)) {
await db.put(table)
}
const response = await db.put(row) const response = await db.put(row)
// for response, calculate the formulas for the enriched row // for response, calculate the formulas for the enriched row
enrichedRow._rev = response.rev enrichedRow._rev = response.rev
@ -158,8 +152,6 @@ export async function finaliseRow(
if (updateFormula) { if (updateFormula) {
await updateRelatedFormula(table, enrichedRow) await updateRelatedFormula(table, enrichedRow)
} }
const squashed = await linkRows.squashLinks(table, enrichedRow, { const squashed = await linkRows.squashLinks(source, enrichedRow)
fromViewId,
})
return { row: enrichedRow, squashed, table } return { row: enrichedRow, squashed, table }
} }

View file

@ -1,6 +1,6 @@
import * as utils from "../../../../db/utils" import * as utils from "../../../../db/utils"
import { context, docIds } from "@budibase/backend-core" import { docIds } from "@budibase/backend-core"
import { import {
Aggregation, Aggregation,
Ctx, Ctx,
@ -20,7 +20,6 @@ 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"
import validateJs from "validate.js" import validateJs from "validate.js"
import { getFullUser } from "../../../../utilities/users"
validateJs.extend(validateJs.validators.datetime, { validateJs.extend(validateJs.validators.datetime, {
parse: function (value: string) { parse: function (value: string) {
@ -60,21 +59,6 @@ export async function processRelationshipFields(
return row return row
} }
export async function findRow(tableId: string, rowId: string) {
const db = context.getAppDB()
let row: Row
// TODO remove special user case in future
if (tableId === utils.InternalTables.USER_METADATA) {
row = await getFullUser(rowId)
} else {
row = await db.get(rowId)
}
if (row.tableId !== tableId) {
throw "Supplied tableId does not match the rows tableId"
}
return row
}
export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } { export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
// top priority, use the URL first // top priority, use the URL first
if (ctx.params?.sourceId) { if (ctx.params?.sourceId) {

View file

@ -113,11 +113,10 @@ export async function bulkImport(
const processed = await inputProcessing(ctx.user?._id, table, row, { const processed = await inputProcessing(ctx.user?._id, table, row, {
noAutoRelationships: true, noAutoRelationships: true,
}) })
parsedRows.push(processed.row) parsedRows.push(processed)
table = processed.table
} }
await handleRequest(Operation.BULK_UPSERT, table._id!, { await handleRequest(Operation.BULK_UPSERT, table, {
rows: parsedRows, rows: parsedRows,
}) })
await events.rows.imported(table, parsedRows.length) await events.rows.imported(table, parsedRows.length)

View file

@ -139,8 +139,7 @@ export async function importToRows(
const processed = await inputProcessing(user?._id, table, row, { const processed = await inputProcessing(user?._id, table, row, {
noAutoRelationships: true, noAutoRelationships: true,
}) })
row = processed.row row = processed
table = processed.table
// However here we must reference the original table, as we want to mutate // However here we must reference the original table, as we want to mutate
// the real schema of the table passed in, not the clone used for // the real schema of the table passed in, not the clone used for

View file

@ -157,7 +157,7 @@ describe.each([
if (isInMemory) { if (isInMemory) {
return dataFilters.search(_.cloneDeep(rows), this.query) return dataFilters.search(_.cloneDeep(rows), this.query)
} else { } else {
return config.api.row.search(this.query.tableId, this.query) return config.api.row.search(this.query.sourceId, this.query)
} }
} }
@ -327,8 +327,8 @@ describe.each([
} }
} }
function expectSearch(query: Omit<RowSearchParams, "tableId">) { function expectSearch(query: Omit<RowSearchParams, "sourceId">) {
return new SearchAssertion({ ...query, tableId: table._id! }) return new SearchAssertion({ ...query, sourceId: table._id! })
} }
function expectQuery(query: SearchFilters) { function expectQuery(query: SearchFilters) {
@ -1898,7 +1898,7 @@ describe.each([
let { rows: fullRowList } = await config.api.row.search( let { rows: fullRowList } = await config.api.row.search(
table._id!, table._id!,
{ {
tableId: table._id!, sourceId: table._id!,
query: {}, query: {},
} }
) )
@ -1909,7 +1909,7 @@ describe.each([
rowCount: number = 0 rowCount: number = 0
do { do {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
limit: 1, limit: 1,
paginate: true, paginate: true,
query: {}, query: {},
@ -1933,7 +1933,7 @@ describe.each([
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
limit: 3, limit: 3,
query: {}, query: {},
bookmark, bookmark,

View file

@ -113,7 +113,7 @@ describe("/templates", () => {
expect(users.name).toBe("Users") expect(users.name).toBe("Users")
const { rows } = await config.api.row.search(agencyProjects._id!, { const { rows } = await config.api.row.search(agencyProjects._id!, {
tableId: agencyProjects._id!, sourceId: agencyProjects._id!,
query: {}, query: {},
}) })

View file

@ -255,31 +255,31 @@ export type SquashTableFields = Record<string, { visibleFieldNames: string[] }>
* @returns The rows after having their links squashed to only contain the ID and primary display. * @returns The rows after having their links squashed to only contain the ID and primary display.
*/ */
export async function squashLinks<T = Row[] | Row>( export async function squashLinks<T = Row[] | Row>(
table: Table, source: Table | ViewV2,
enriched: T, enriched: T
options?: {
fromViewId?: string
}
): Promise<T> { ): Promise<T> {
const allowRelationshipSchemas = await features.flags.isEnabled( const allowRelationshipSchemas = await features.flags.isEnabled(
FeatureFlag.ENRICHED_RELATIONSHIPS FeatureFlag.ENRICHED_RELATIONSHIPS
) )
let viewSchema: Record<string, ViewUIFieldMetadata> = {} let viewSchema: Record<string, ViewUIFieldMetadata> = {}
if (options?.fromViewId) { if (sdk.views.isView(source)) {
const view = Object.values(table.views || {}).find( if (helpers.views.isCalculationView(source)) {
(v): v is ViewV2 => sdk.views.isV2(v) && v.id === options?.fromViewId
)
if (view && helpers.views.isCalculationView(view)) {
return enriched return enriched
} }
if (allowRelationshipSchemas && view) { if (allowRelationshipSchemas) {
viewSchema = view.schema || {} viewSchema = source.schema || {}
} }
} }
let table: Table
if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id)
} else {
table = source
}
// will populate this as we find them // will populate this as we find them
const linkedTables = [table] const linkedTables = [table]
const isArray = Array.isArray(enriched) const isArray = Array.isArray(enriched)

View file

@ -219,7 +219,7 @@ describe("Google Sheets Integration", () => {
}) })
let resp = await config.api.row.search(table._id!, { let resp = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: {}, query: {},
paginate: true, paginate: true,
limit: 10, limit: 10,
@ -228,7 +228,7 @@ describe("Google Sheets Integration", () => {
while (resp.hasNextPage) { while (resp.hasNextPage) {
resp = await config.api.row.search(table._id!, { resp = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: {}, query: {},
paginate: true, paginate: true,
limit: 10, limit: 10,
@ -637,7 +637,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with equals filter", async () => { it("should be able to find rows with equals filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
equal: { equal: {
name: "Foo", name: "Foo",
@ -651,7 +651,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with not equals filter", async () => { it("should be able to find rows with not equals filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
notEqual: { notEqual: {
name: "Foo", name: "Foo",
@ -666,7 +666,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with empty filter", async () => { it("should be able to find rows with empty filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
empty: { empty: {
name: null, name: null,
@ -679,7 +679,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with not empty filter", async () => { it("should be able to find rows with not empty filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
notEmpty: { notEmpty: {
name: null, name: null,
@ -692,7 +692,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with one of filter", async () => { it("should be able to find rows with one of filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
oneOf: { oneOf: {
name: ["Foo", "Bar"], name: ["Foo", "Bar"],
@ -707,7 +707,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with fuzzy filter", async () => { it("should be able to find rows with fuzzy filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
fuzzy: { fuzzy: {
name: "oo", name: "oo",
@ -721,7 +721,7 @@ describe("Google Sheets Integration", () => {
it("should be able to find rows with range filter", async () => { it("should be able to find rows with range filter", async () => {
const response = await config.api.row.search(table._id!, { const response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { query: {
range: { range: {
name: { name: {
@ -750,7 +750,7 @@ describe("Google Sheets Integration", () => {
}) })
let response = await config.api.row.search(table._id!, { let response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { equal: { name: "Unique value!" } }, query: { equal: { name: "Unique value!" } },
paginate: true, paginate: true,
limit: 10, limit: 10,
@ -759,7 +759,7 @@ describe("Google Sheets Integration", () => {
while (response.hasNextPage) { while (response.hasNextPage) {
response = await config.api.row.search(table._id!, { response = await config.api.row.search(table._id!, {
tableId: table._id!, sourceId: table._id!,
query: { equal: { name: "Unique value!" } }, query: { equal: { name: "Unique value!" } },
paginate: true, paginate: true,
limit: 10, limit: 10,

View file

@ -14,7 +14,6 @@ import {
outputProcessing, outputProcessing,
} from "../../../utilities/rowProcessor" } from "../../../utilities/rowProcessor"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"
import isEqual from "lodash/fp/isEqual"
import { tryExtractingTableAndViewId } from "./utils" import { tryExtractingTableAndViewId } from "./utils"
export async function getRow( export async function getRow(
@ -55,11 +54,7 @@ export async function save(
source = await sdk.tables.getTable(tableId) source = await sdk.tables.getTable(tableId)
} }
const { table: updatedTable, row } = await inputProcessing( const row = await inputProcessing(userId, cloneDeep(source), inputs)
userId,
cloneDeep(source),
inputs
)
const validateResult = await sdk.rows.utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
@ -73,10 +68,6 @@ export async function save(
row, row,
}) })
if (sdk.tables.isTable(source) && !isEqual(source, updatedTable)) {
await sdk.tables.saveTable(updatedTable)
}
const rowId = response.row._id const rowId = response.row._id
if (rowId) { if (rowId) {
const row = await getRow(source, rowId, { const row = await getRow(source, rowId, {

View file

@ -1,5 +1,5 @@
import { context, db } from "@budibase/backend-core" import { context, db } from "@budibase/backend-core"
import { Row } from "@budibase/types" import { Row, Table, ViewV2 } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"
import { finaliseRow } from "../../../api/controllers/row/staticFormula" import { finaliseRow } from "../../../api/controllers/row/staticFormula"
@ -10,7 +10,7 @@ import {
import * as linkRows from "../../../db/linkedRows" import * as linkRows from "../../../db/linkedRows"
import { InternalTables } from "../../../db/utils" import { InternalTables } from "../../../db/utils"
import { getFullUser } from "../../../utilities/users" import { getFullUser } from "../../../utilities/users"
import { tryExtractingTableAndViewId } from "./utils" import { getSource, tryExtractingTableAndViewId } from "./utils"
export async function save( export async function save(
tableOrViewId: string, tableOrViewId: string,
@ -20,21 +20,28 @@ export async function save(
const { tableId, viewId } = tryExtractingTableAndViewId(tableOrViewId) const { tableId, viewId } = tryExtractingTableAndViewId(tableOrViewId)
inputs.tableId = tableId inputs.tableId = tableId
let source: Table | ViewV2
let table: Table
if (viewId) {
source = await sdk.views.get(viewId)
table = await sdk.views.getTable(viewId)
} else {
source = await sdk.tables.getTable(tableId)
table = source
}
if (!inputs._rev && !inputs._id) { if (!inputs._rev && !inputs._id) {
inputs._id = db.generateRowID(inputs.tableId) inputs._id = db.generateRowID(inputs.tableId)
} }
// this returns the table and row incase they have been updated
const dbTable = await sdk.tables.getTable(inputs.tableId)
// need to copy the table so it can be differenced on way out // need to copy the table so it can be differenced on way out
const tableClone = cloneDeep(dbTable) const sourceClone = cloneDeep(source)
let { table, row } = await inputProcessing(userId, tableClone, inputs) let row = await inputProcessing(userId, sourceClone, inputs)
const validateResult = await sdk.rows.utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
table, source,
}) })
if (!validateResult.valid) { if (!validateResult.valid) {
@ -49,24 +56,18 @@ export async function save(
table, table,
})) as Row })) as Row
return finaliseRow(table, row, { return finaliseRow(table, row, { updateFormula: true })
oldTable: dbTable, }
updateFormula: true,
fromViewId: viewId, export async function find(sourceId: string, rowId: string): Promise<Row> {
const source = await getSource(sourceId)
return await outputProcessing(source, await findRow(sourceId, rowId), {
squash: true,
}) })
} }
export async function find(tableOrViewId: string, rowId: string): Promise<Row> { export async function findRow(sourceId: string, rowId: string) {
const { tableId, viewId } = tryExtractingTableAndViewId(tableOrViewId) const { tableId } = tryExtractingTableAndViewId(sourceId)
const table = await sdk.tables.getTable(tableId)
let row = await findRow(tableId, rowId)
row = await outputProcessing(table, row, { squash: true, fromViewId: viewId })
return row
}
async function findRow(tableId: string, rowId: string) {
const db = context.getAppDB() const db = context.getAppDB()
let row: Row let row: Row
// TODO remove special user case in future // TODO remove special user case in future

View file

@ -64,7 +64,7 @@ export async function exportRows(
result = await outputProcessing(table, response) result = await outputProcessing(table, response)
} else if (query) { } else if (query) {
let searchResponse = await sdk.rows.search({ let searchResponse = await sdk.rows.search({
tableId, sourceId: tableId,
query, query,
sort, sort,
sortOrder, sortOrder,

View file

@ -8,21 +8,30 @@ import {
SortType, SortType,
Table, Table,
User, User,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { getGlobalUsersFromMetadata } from "../../../../../utilities/global" import { getGlobalUsersFromMetadata } from "../../../../../utilities/global"
import { outputProcessing } from "../../../../../utilities/rowProcessor" import { outputProcessing } from "../../../../../utilities/rowProcessor"
import pick from "lodash/pick" import pick from "lodash/pick"
import sdk from "../../../../"
export async function search( export async function search(
options: RowSearchParams, options: RowSearchParams,
table: Table source: Table | ViewV2
): Promise<SearchResponse<Row>> { ): Promise<SearchResponse<Row>> {
const { tableId } = options const { sourceId } = options
let table: Table
if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id)
} else {
table = source
}
const { paginate, query } = options const { paginate, query } = options
const params: RowSearchParams = { const params: RowSearchParams = {
tableId: options.tableId, sourceId: options.sourceId,
sort: options.sort, sort: options.sort,
sortOrder: options.sortOrder, sortOrder: options.sortOrder,
sortType: options.sortType, sortType: options.sortType,
@ -50,7 +59,7 @@ export async function search(
// Enrich search results with relationships // Enrich search results with relationships
if (response.rows && response.rows.length) { if (response.rows && response.rows.length) {
// enrich with global users if from users table // enrich with global users if from users table
if (tableId === InternalTables.USER_METADATA) { if (sourceId === InternalTables.USER_METADATA) {
response.rows = await getGlobalUsersFromMetadata(response.rows as User[]) response.rows = await getGlobalUsersFromMetadata(response.rows as User[])
} }
@ -59,9 +68,8 @@ export async function search(
response.rows = response.rows.map((r: any) => pick(r, fields)) response.rows = response.rows.map((r: any) => pick(r, fields))
} }
response.rows = await outputProcessing(table, response.rows, { response.rows = await outputProcessing(source, response.rows, {
squash: true, squash: true,
fromViewId: options.viewId,
}) })
} }

View file

@ -15,6 +15,7 @@ import {
SortType, SortType,
SqlClient, SqlClient,
Table, Table,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { import {
buildInternalRelationships, buildInternalRelationships,
@ -292,11 +293,18 @@ function resyncDefinitionsRequired(status: number, message: string) {
export async function search( export async function search(
options: RowSearchParams, options: RowSearchParams,
table: Table, source: Table | ViewV2,
opts?: { retrying?: boolean } opts?: { retrying?: boolean }
): Promise<SearchResponse<Row>> { ): Promise<SearchResponse<Row>> {
let { paginate, query, ...params } = cloneDeep(options) let { paginate, query, ...params } = cloneDeep(options)
let table: Table
if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id)
} else {
table = source
}
const allTables = await sdk.tables.getAllInternalTables() const allTables = await sdk.tables.getAllInternalTables()
const allTablesMap = buildTableMap(allTables) const allTablesMap = buildTableMap(allTables)
// make sure we have the mapped/latest table // make sure we have the mapped/latest table
@ -406,7 +414,6 @@ export async function search(
let finalRows = await outputProcessing(table, processed, { let finalRows = await outputProcessing(table, processed, {
preserveLinks: true, preserveLinks: true,
squash: true, squash: true,
fromViewId: options.viewId,
aggregations: options.aggregations, aggregations: options.aggregations,
}) })

View file

@ -122,7 +122,7 @@ describe.each([
it("querying by fields will always return data attribute columns", async () => { it("querying by fields will always return data attribute columns", async () => {
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
const { rows } = await search({ const { rows } = await search({
tableId: table._id!, sourceId: table._id!,
query: {}, query: {},
fields: ["name", "age"], fields: ["name", "age"],
}) })
@ -142,7 +142,7 @@ describe.each([
it("will decode _id in oneOf query", async () => { it("will decode _id in oneOf query", async () => {
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
const result = await search({ const result = await search({
tableId: table._id!, sourceId: table._id!,
query: { query: {
oneOf: { oneOf: {
_id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"], _id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"],
@ -174,7 +174,7 @@ describe.each([
}, },
}) })
const result = await search({ const result = await search({
tableId: table._id!, sourceId: table._id!,
query: {}, query: {},
}) })
expect(result.rows).toHaveLength(10) expect(result.rows).toHaveLength(10)
@ -205,7 +205,7 @@ describe.each([
}, },
}) })
const result = await search({ const result = await search({
tableId: table._id!, sourceId: table._id!,
query: {}, query: {},
fields: ["name", "age"], fields: ["name", "age"],
}) })
@ -229,7 +229,7 @@ describe.each([
async (queryFields, expectedRows) => { async (queryFields, expectedRows) => {
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
const { rows } = await search({ const { rows } = await search({
tableId: table._id!, sourceId: table._id!,
query: { query: {
$or: { $or: {
conditions: [ conditions: [

View file

@ -48,7 +48,7 @@ describe.each([tableWithUserCol, tableWithUsersCol])(
it("should be able to map ro_ to global user IDs", () => { it("should be able to map ro_ to global user IDs", () => {
const params: RowSearchParams = { const params: RowSearchParams = {
tableId, sourceId: tableId,
query: { query: {
equal: { equal: {
"1:user": userMedataId, "1:user": userMedataId,
@ -61,7 +61,7 @@ describe.each([tableWithUserCol, tableWithUsersCol])(
it("should handle array of user IDs", () => { it("should handle array of user IDs", () => {
const params: RowSearchParams = { const params: RowSearchParams = {
tableId, sourceId: tableId,
query: { query: {
oneOf: { oneOf: {
"1:user": [userMedataId, globalUserId], "1:user": [userMedataId, globalUserId],
@ -78,7 +78,7 @@ describe.each([tableWithUserCol, tableWithUsersCol])(
it("shouldn't change any other input", () => { it("shouldn't change any other input", () => {
const email = "test@example.com" const email = "test@example.com"
const params: RowSearchParams = { const params: RowSearchParams = {
tableId, sourceId: tableId,
query: { query: {
equal: { equal: {
"1:user": email, "1:user": email,
@ -90,10 +90,8 @@ describe.each([tableWithUserCol, tableWithUsersCol])(
}) })
it("shouldn't error if no query supplied", () => { it("shouldn't error if no query supplied", () => {
const params: any = { // @ts-expect-error - intentionally passing in a bad type
tableId, const output = searchInputMapping(col, { sourceId: tableId })
}
const output = searchInputMapping(col, params)
expect(output.query).toBeUndefined() expect(output.query).toBeUndefined()
}) })
} }

View file

@ -33,7 +33,7 @@ describe("validate", () => {
it("should accept empty values", async () => { it("should accept empty values", async () => {
const row = {} const row = {}
const table = getTable() const table = getTable()
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
expect(output.errors).toEqual({}) expect(output.errors).toEqual({})
}) })
@ -43,7 +43,7 @@ describe("validate", () => {
time: `${hour()}:${minute()}`, time: `${hour()}:${minute()}`,
} }
const table = getTable() const table = getTable()
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
}) })
@ -52,7 +52,7 @@ describe("validate", () => {
time: `${hour()}:${minute()}:${second()}`, time: `${hour()}:${minute()}:${second()}`,
} }
const table = getTable() const table = getTable()
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
}) })
@ -67,7 +67,7 @@ describe("validate", () => {
table.schema.time.constraints = { table.schema.time.constraints = {
presence: true, presence: true,
} }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ time: ['"time" is not a valid time'] }) expect(output.errors).toEqual({ time: ['"time" is not a valid time'] })
}) })
@ -91,7 +91,7 @@ describe("validate", () => {
`${generator.integer({ min: 11, max: 23 })}:${minute()}`, `${generator.integer({ min: 11, max: 23 })}:${minute()}`,
])("should accept values after config value (%s)", async time => { ])("should accept values after config value (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
}) })
@ -100,7 +100,7 @@ describe("validate", () => {
`${generator.integer({ min: 0, max: 9 })}:${minute()}`, `${generator.integer({ min: 0, max: 9 })}:${minute()}`,
])("should reject values before config value (%s)", async time => { ])("should reject values before config value (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no earlier than 10:00"], time: ["must be no earlier than 10:00"],
@ -125,7 +125,7 @@ describe("validate", () => {
`${generator.integer({ min: 0, max: 12 })}:${minute()}`, `${generator.integer({ min: 0, max: 12 })}:${minute()}`,
])("should accept values before config value (%s)", async time => { ])("should accept values before config value (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
}) })
@ -134,7 +134,7 @@ describe("validate", () => {
`${generator.integer({ min: 16, max: 23 })}:${minute()}`, `${generator.integer({ min: 16, max: 23 })}:${minute()}`,
])("should reject values after config value (%s)", async time => { ])("should reject values after config value (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no later than 15:16:17"], time: ["must be no later than 15:16:17"],
@ -156,7 +156,7 @@ describe("validate", () => {
"should accept values in range (%s)", "should accept values in range (%s)",
async time => { async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
} }
) )
@ -166,7 +166,7 @@ describe("validate", () => {
`${generator.integer({ min: 0, max: 9 })}:${minute()}`, `${generator.integer({ min: 0, max: 9 })}:${minute()}`,
])("should reject values before range (%s)", async time => { ])("should reject values before range (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no earlier than 10:00"], time: ["must be no earlier than 10:00"],
@ -178,7 +178,7 @@ describe("validate", () => {
`${generator.integer({ min: 16, max: 23 })}:${minute()}`, `${generator.integer({ min: 16, max: 23 })}:${minute()}`,
])("should reject values after range (%s)", async time => { ])("should reject values after range (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no later than 15:00"], time: ["must be no later than 15:00"],
@ -199,7 +199,7 @@ describe("validate", () => {
"should accept values in range (%s)", "should accept values in range (%s)",
async time => { async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
} }
) )
@ -208,7 +208,7 @@ describe("validate", () => {
"should reject values out range (%s)", "should reject values out range (%s)",
async time => { async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no later than 10:00"], time: ["must be no later than 10:00"],
@ -226,7 +226,7 @@ describe("validate", () => {
table.schema.time.constraints = { table.schema.time.constraints = {
presence: true, presence: true,
} }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ time: ["can't be blank"] }) expect(output.errors).toEqual({ time: ["can't be blank"] })
}) })
@ -237,7 +237,7 @@ describe("validate", () => {
table.schema.time.constraints = { table.schema.time.constraints = {
presence: true, presence: true,
} }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ time: ["can't be blank"] }) expect(output.errors).toEqual({ time: ["can't be blank"] })
}) })
@ -257,7 +257,7 @@ describe("validate", () => {
"should accept values in range (%s)", "should accept values in range (%s)",
async time => { async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
} }
) )
@ -267,7 +267,7 @@ describe("validate", () => {
`${generator.integer({ min: 0, max: 9 })}:${minute()}`, `${generator.integer({ min: 0, max: 9 })}:${minute()}`,
])("should reject values before range (%s)", async time => { ])("should reject values before range (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no earlier than 10:00"], time: ["must be no earlier than 10:00"],
@ -279,7 +279,7 @@ describe("validate", () => {
`${generator.integer({ min: 16, max: 23 })}:${minute()}`, `${generator.integer({ min: 16, max: 23 })}:${minute()}`,
])("should reject values after range (%s)", async time => { ])("should reject values after range (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no later than 15:00"], time: ["must be no later than 15:00"],
@ -301,7 +301,7 @@ describe("validate", () => {
"should accept values in range (%s)", "should accept values in range (%s)",
async time => { async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(true) expect(output.valid).toBe(true)
} }
) )
@ -311,7 +311,7 @@ describe("validate", () => {
`${generator.integer({ min: 0, max: 9 })}:${minute()}`, `${generator.integer({ min: 0, max: 9 })}:${minute()}`,
])("should reject values before range (%s)", async time => { ])("should reject values before range (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no earlier than 10:00"], time: ["must be no earlier than 10:00"],
@ -323,7 +323,7 @@ describe("validate", () => {
`${generator.integer({ min: 16, max: 23 })}:${minute()}`, `${generator.integer({ min: 16, max: 23 })}:${minute()}`,
])("should reject values after range (%s)", async time => { ])("should reject values after range (%s)", async time => {
const row = { time } const row = { time }
const output = await validate({ table, tableId: table._id!, row }) const output = await validate({ source: table, row })
expect(output.valid).toBe(false) expect(output.valid).toBe(false)
expect(output.errors).toEqual({ expect(output.errors).toEqual({
time: ["must be no later than 15:00"], time: ["must be no later than 15:00"],

View file

@ -321,3 +321,10 @@ export function tryExtractingTableAndViewId(tableOrViewId: string) {
return { tableId: tableOrViewId } return { tableId: tableOrViewId }
} }
export function getSource(tableOrViewId: string) {
if (docIds.isViewId(tableOrViewId)) {
return sdk.views.get(tableOrViewId)
}
return sdk.tables.getTable(tableOrViewId)
}

View file

@ -40,7 +40,8 @@ export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
return pickApi(tableId).getEnriched(viewId) return pickApi(tableId).getEnriched(viewId)
} }
export async function getTable(viewId: string): Promise<Table> { export async function getTable(view: string | ViewV2): Promise<Table> {
const viewId = typeof view === "string" ? view : view.id
const cached = context.getTableForView(viewId) const cached = context.getTableForView(viewId)
if (cached) { if (cached) {
return cached return cached

View file

@ -73,6 +73,7 @@ export async function processAutoColumn(
// check its not user table, or whether any of the processing options have been disabled // check its not user table, or whether any of the processing options have been disabled
const shouldUpdateUserFields = const shouldUpdateUserFields =
!isUserTable && !opts?.reprocessing && !opts?.noAutoRelationships && !noUser !isUserTable && !opts?.reprocessing && !opts?.noAutoRelationships && !noUser
let tableMutated = false
for (let [key, schema] of Object.entries(table.schema)) { for (let [key, schema] of Object.entries(table.schema)) {
if (!schema.autocolumn) { if (!schema.autocolumn) {
continue continue
@ -105,10 +106,17 @@ export async function processAutoColumn(
row[key] = schema.lastID + 1 row[key] = schema.lastID + 1
schema.lastID++ schema.lastID++
table.schema[key] = schema table.schema[key] = schema
tableMutated = true
} }
break break
} }
} }
if (tableMutated) {
const db = context.getAppDB()
const resp = await db.put(table)
table._rev = resp.rev
}
} }
async function processDefaultValues(table: Table, row: Row) { async function processDefaultValues(table: Table, row: Row) {
@ -235,8 +243,7 @@ export async function inputProcessing(
await processAutoColumn(userId, table, clonedRow, opts) await processAutoColumn(userId, table, clonedRow, opts)
await processDefaultValues(table, clonedRow) await processDefaultValues(table, clonedRow)
return clonedRow
return { table, row: clonedRow }
} }
/** /**
@ -271,6 +278,14 @@ export async function outputProcessing<T extends Row[] | Row>(
} else { } else {
safeRows = rows safeRows = rows
} }
let table: Table
if (sdk.views.isView(source)) {
table = await sdk.views.getTable(source.id)
} else {
table = source
}
// attach any linked row information // attach any linked row information
let enriched = !opts.preserveLinks let enriched = !opts.preserveLinks
? await linkRows.attachFullLinkedDocs(table.schema, safeRows, { ? await linkRows.attachFullLinkedDocs(table.schema, safeRows, {
@ -360,9 +375,7 @@ export async function outputProcessing<T extends Row[] | Row>(
enriched = await processFormulas(table, enriched, { dynamic: true }) enriched = await processFormulas(table, enriched, { dynamic: true })
if (opts.squash) { if (opts.squash) {
enriched = await linkRows.squashLinks(table, enriched, { enriched = await linkRows.squashLinks(source, enriched)
fromViewId: opts?.fromViewId,
})
} }
// remove null properties to match internal API // remove null properties to match internal API

View file

@ -65,7 +65,7 @@ describe("rowProcessor - inputProcessing", () => {
processInputBBReferenceMock.mockResolvedValue(user) processInputBBReferenceMock.mockResolvedValue(user)
const { row } = await inputProcessing(userId, table, newRow) const row = await inputProcessing(userId, table, newRow)
expect(bbReferenceProcessor.processInputBBReference).toHaveBeenCalledTimes( expect(bbReferenceProcessor.processInputBBReference).toHaveBeenCalledTimes(
1 1
@ -117,7 +117,7 @@ describe("rowProcessor - inputProcessing", () => {
processInputBBReferencesMock.mockResolvedValue(user) processInputBBReferencesMock.mockResolvedValue(user)
const { row } = await inputProcessing(userId, table, newRow) const row = await inputProcessing(userId, table, newRow)
expect(bbReferenceProcessor.processInputBBReferences).toHaveBeenCalledTimes( expect(bbReferenceProcessor.processInputBBReferences).toHaveBeenCalledTimes(
1 1
@ -164,7 +164,7 @@ describe("rowProcessor - inputProcessing", () => {
name: "Jack", name: "Jack",
} }
const { row } = await inputProcessing(userId, table, newRow) const row = await inputProcessing(userId, table, newRow)
expect(bbReferenceProcessor.processInputBBReferences).not.toHaveBeenCalled() expect(bbReferenceProcessor.processInputBBReferences).not.toHaveBeenCalled()
expect(row).toEqual({ ...newRow, user: undefined }) expect(row).toEqual({ ...newRow, user: undefined })
@ -207,7 +207,7 @@ describe("rowProcessor - inputProcessing", () => {
user: userValue, user: userValue,
} }
const { row } = await inputProcessing(userId, table, newRow) const row = await inputProcessing(userId, table, newRow)
if (userValue === undefined) { if (userValue === undefined) {
// The 'user' field is omitted // The 'user' field is omitted
@ -262,7 +262,7 @@ describe("rowProcessor - inputProcessing", () => {
user: "123", user: "123",
} }
const { row } = await inputProcessing(userId, table, newRow) const row = await inputProcessing(userId, table, newRow)
expect(bbReferenceProcessor.processInputBBReferences).not.toHaveBeenCalled() expect(bbReferenceProcessor.processInputBBReferences).not.toHaveBeenCalled()
expect(row).toEqual({ expect(row).toEqual({