+ System logs
Download your latest logs to share with the Budibase team
diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts
index 6bae6afd48..802c70b6cb 100644
--- a/packages/server/src/api/controllers/row/external.ts
+++ b/packages/server/src/api/controllers/row/external.ts
@@ -8,26 +8,13 @@ import {
Datasource,
IncludeRelationship,
Operation,
+ PatchRowRequest,
+ PatchRowResponse,
Row,
Table,
UserCtx,
} from "@budibase/types"
import sdk from "../../../sdk"
-import * as utils from "./utils"
-
-async function getRow(
- tableId: string,
- rowId: string,
- opts?: { relationships?: boolean }
-) {
- const response = (await handleRequest(Operation.READ, tableId, {
- id: breakRowIdField(rowId),
- includeSqlRelationships: opts?.relationships
- ? IncludeRelationship.INCLUDE
- : IncludeRelationship.EXCLUDE,
- })) as Row[]
- return response ? response[0] : response
-}
export async function handleRequest(
operation: Operation,
@@ -55,14 +42,12 @@ export async function handleRequest(
)
}
-export async function patch(ctx: UserCtx) {
- const inputs = ctx.request.body
+export async function patch(ctx: UserCtx) {
const tableId = ctx.params.tableId
- const id = inputs._id
- // don't save the ID to db
- delete inputs._id
- const validateResult = await utils.validate({
- row: inputs,
+ const { id, ...rowData } = ctx.request.body
+
+ const validateResult = await sdk.rows.utils.validate({
+ row: rowData,
tableId,
})
if (!validateResult.valid) {
@@ -70,9 +55,11 @@ export async function patch(ctx: UserCtx) {
}
const response = await handleRequest(Operation.UPDATE, tableId, {
id: breakRowIdField(id),
- row: inputs,
+ row: rowData,
+ })
+ const row = await sdk.rows.external.getRow(tableId, id, {
+ relationships: true,
})
- const row = await getRow(tableId, id, { relationships: true })
const table = await sdk.tables.getTable(tableId)
return {
...response,
@@ -84,7 +71,7 @@ export async function patch(ctx: UserCtx) {
export async function save(ctx: UserCtx) {
const inputs = ctx.request.body
const tableId = ctx.params.tableId
- const validateResult = await utils.validate({
+ const validateResult = await sdk.rows.utils.validate({
row: inputs,
tableId,
})
@@ -97,7 +84,9 @@ export async function save(ctx: UserCtx) {
const responseRow = response as { row: Row }
const rowId = responseRow.row._id
if (rowId) {
- const row = await getRow(tableId, rowId, { relationships: true })
+ const row = await sdk.rows.external.getRow(tableId, rowId, {
+ relationships: true,
+ })
return {
...response,
row,
@@ -110,7 +99,7 @@ export async function save(ctx: UserCtx) {
export async function find(ctx: UserCtx) {
const id = ctx.params.rowId
const tableId = ctx.params.tableId
- return getRow(tableId, id)
+ return sdk.rows.external.getRow(tableId, id)
}
export async function destroy(ctx: UserCtx) {
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 79cd5fbfe0..4cbf17d844 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -9,6 +9,8 @@ import {
DeleteRow,
DeleteRows,
Row,
+ PatchRowRequest,
+ PatchRowResponse,
SearchResponse,
SortOrder,
SortType,
@@ -29,7 +31,9 @@ function pickApi(tableId: any) {
return internal
}
-export async function patch(ctx: any): Promise {
+export async function patch(
+ ctx: UserCtx
+): Promise {
const appId = ctx.appId
const tableId = utils.getTableId(ctx)
const body = ctx.request.body
@@ -38,7 +42,7 @@ export async function patch(ctx: any): Promise {
return save(ctx)
}
try {
- const { row, table } = await quotas.addQuery(
+ const { row, table } = await quotas.addQuery(
() => pickApi(tableId).patch(ctx),
{
datasourceId: tableId,
@@ -53,7 +57,7 @@ export async function patch(ctx: any): Promise {
ctx.message = `${table.name} updated successfully.`
ctx.body = row
gridSocket?.emitRowUpdate(ctx, row)
- } catch (err) {
+ } catch (err: any) {
ctx.throw(400, err)
}
}
@@ -78,6 +82,7 @@ export const save = async (ctx: any) => {
ctx.body = row || squashed
gridSocket?.emitRowUpdate(ctx, row || squashed)
}
+
export async function fetchView(ctx: any) {
const tableId = utils.getTableId(ctx)
const viewName = decodeURIComponent(ctx.params.viewName)
@@ -267,7 +272,7 @@ export async function searchView(ctx: Ctx) {
undefined
ctx.status = 200
- ctx.body = await quotas.addQuery(
+ const result = await quotas.addQuery(
() =>
sdk.rows.search({
tableId: view.tableId,
@@ -279,6 +284,9 @@ export async function searchView(ctx: Ctx) {
datasourceId: view.tableId,
}
)
+
+ result.rows.forEach(r => (r._viewId = view.id))
+ ctx.body = result
}
export async function validate(ctx: Ctx) {
@@ -287,7 +295,7 @@ export async function validate(ctx: Ctx) {
if (isExternalTable(tableId)) {
ctx.body = { valid: true }
} else {
- ctx.body = await utils.validate({
+ ctx.body = await sdk.rows.utils.validate({
row: ctx.request.body,
tableId,
})
diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts
index d56ba3f14a..1153461b89 100644
--- a/packages/server/src/api/controllers/row/internal.ts
+++ b/packages/server/src/api/controllers/row/internal.ts
@@ -15,19 +15,26 @@ import * as utils from "./utils"
import { cloneDeep } from "lodash/fp"
import { context, db as dbCore } from "@budibase/backend-core"
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
-import { UserCtx, LinkDocumentValue, Row, Table } from "@budibase/types"
+import {
+ UserCtx,
+ LinkDocumentValue,
+ Row,
+ Table,
+ PatchRowRequest,
+ PatchRowResponse,
+} from "@budibase/types"
import sdk from "../../../sdk"
-export async function patch(ctx: UserCtx) {
+export async function patch(ctx: UserCtx) {
const inputs = ctx.request.body
const tableId = inputs.tableId
const isUserTable = tableId === InternalTables.USER_METADATA
let oldRow
+ const dbTable = await sdk.tables.getTable(tableId)
try {
- let dbTable = await sdk.tables.getTable(tableId)
oldRow = await outputProcessing(
dbTable,
- await utils.findRow(ctx, tableId, inputs._id)
+ await utils.findRow(ctx, tableId, inputs._id!)
)
} catch (err) {
if (isUserTable) {
@@ -40,7 +47,7 @@ export async function patch(ctx: UserCtx) {
throw "Row does not exist"
}
}
- let dbTable = await sdk.tables.getTable(tableId)
+
// need to build up full patch fields before coerce
let combinedRow: any = cloneDeep(oldRow)
for (let key of Object.keys(inputs)) {
@@ -53,7 +60,7 @@ export async function patch(ctx: UserCtx) {
// this returns the table and row incase they have been updated
let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow)
- const validateResult = await utils.validate({
+ const validateResult = await sdk.rows.utils.validate({
row,
table,
})
@@ -74,7 +81,7 @@ export async function patch(ctx: UserCtx) {
if (isUserTable) {
// the row has been updated, need to put it into the ctx
- ctx.request.body = row
+ ctx.request.body = row as any
await userController.updateMetadata(ctx)
return { row: ctx.body as Row, table }
}
@@ -102,7 +109,7 @@ export async function save(ctx: UserCtx) {
let { table, row } = inputProcessing(ctx.user, tableClone, inputs)
- const validateResult = await utils.validate({
+ const validateResult = await sdk.rows.utils.validate({
row,
table,
})
diff --git a/packages/server/src/api/controllers/user.ts b/packages/server/src/api/controllers/user.ts
index 1a2a3850ce..dbbfc5c586 100644
--- a/packages/server/src/api/controllers/user.ts
+++ b/packages/server/src/api/controllers/user.ts
@@ -1,6 +1,5 @@
-import { generateUserMetadataID, generateUserFlagID } from "../../db/utils"
+import { generateUserFlagID } from "../../db/utils"
import { InternalTables } from "../../db/utils"
-import { getGlobalUsers } from "../../utilities/global"
import { getFullUser } from "../../utilities/users"
import { context } from "@budibase/backend-core"
import { Ctx, UserCtx } from "@budibase/types"
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 6a5cfa77a2..dbc417a5b5 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -5,7 +5,7 @@ tk.freeze(timestamp)
import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities"
const { basicRow } = setup.structures
-import { context, db, tenancy } from "@budibase/backend-core"
+import { context, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
import {
QuotaUsageType,
@@ -16,6 +16,7 @@ import {
FieldType,
SortType,
SortOrder,
+ PatchRowRequest,
} from "@budibase/types"
import {
expectAnyInternalColsAttributes,
@@ -399,17 +400,12 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .patch(`/api/${table._id}/rows`)
- .send({
- _id: existing._id,
- _rev: existing._rev,
- tableId: table._id,
- name: "Updated Name",
- })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.patch(table._id!, {
+ _id: existing._id!,
+ _rev: existing._rev!,
+ tableId: table._id!,
+ name: "Updated Name",
+ })
expect((res as any).res.statusMessage).toEqual(
`${table.name} updated successfully.`
@@ -430,16 +426,16 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- await request
- .patch(`/api/${table._id}/rows`)
- .send({
- _id: existing._id,
- _rev: existing._rev,
- tableId: table._id,
+ await config.api.row.patch(
+ table._id!,
+ {
+ _id: existing._id!,
+ _rev: existing._rev!,
+ tableId: table._id!,
name: 1,
- })
- .set(config.defaultHeaders())
- .expect(400)
+ },
+ { expectStatus: 400 }
+ )
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
@@ -986,16 +982,17 @@ describe("/rows", () => {
)
}
- const createViewResponse = await config.api.viewV2.create({
+ const view = await config.api.viewV2.create({
columns: { name: { visible: true } },
})
- const response = await config.api.viewV2.search(createViewResponse.id)
+ const response = await config.api.viewV2.search(view.id)
expect(response.body.rows).toHaveLength(10)
expect(response.body.rows).toEqual(
expect.arrayContaining(
rows.map(r => ({
...expectAnyInternalColsAttributes,
+ _viewId: view.id,
name: r.name,
}))
)
diff --git a/packages/server/src/sdk/app/rows/external.ts b/packages/server/src/sdk/app/rows/external.ts
new file mode 100644
index 0000000000..568bd07e9d
--- /dev/null
+++ b/packages/server/src/sdk/app/rows/external.ts
@@ -0,0 +1,17 @@
+import { IncludeRelationship, Operation, Row } from "@budibase/types"
+import { handleRequest } from "../../../api/controllers/row/external"
+import { breakRowIdField } from "../../../integrations/utils"
+
+export async function getRow(
+ tableId: string,
+ rowId: string,
+ opts?: { relationships?: boolean }
+) {
+ const response = (await handleRequest(Operation.READ, tableId, {
+ id: breakRowIdField(rowId),
+ includeSqlRelationships: opts?.relationships
+ ? IncludeRelationship.INCLUDE
+ : IncludeRelationship.EXCLUDE,
+ })) as Row[]
+ return response ? response[0] : response
+}
diff --git a/packages/server/src/sdk/app/rows/index.ts b/packages/server/src/sdk/app/rows/index.ts
index 1ba91b134f..ea501e93d9 100644
--- a/packages/server/src/sdk/app/rows/index.ts
+++ b/packages/server/src/sdk/app/rows/index.ts
@@ -2,10 +2,12 @@ import * as attachments from "./attachments"
import * as rows from "./rows"
import * as search from "./search"
import * as utils from "./utils"
+import * as external from "./external"
export default {
...attachments,
...rows,
...search,
- utils: utils,
+ utils,
+ external,
}
diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts
index 5a29541705..e7f0aadfd6 100644
--- a/packages/server/src/sdk/app/rows/search/internal.ts
+++ b/packages/server/src/sdk/app/rows/search/internal.ts
@@ -147,8 +147,8 @@ export async function exportRows(
export async function fetch(tableId: string) {
const db = context.getAppDB()
- let table = await sdk.tables.getTable(tableId)
- let rows = await getRawTableData(db, tableId)
+ const table = await sdk.tables.getTable(tableId)
+ const rows = await getRawTableData(db, tableId)
const result = await outputProcessing(table, rows)
return result
}
@@ -171,7 +171,7 @@ async function getRawTableData(db: Database, tableId: string) {
export async function fetchView(
viewName: string,
options: { calculation: string; group: string; field: string }
-) {
+): Promise {
// if this is a table view being looked for just transfer to that
if (viewName.startsWith(DocumentType.TABLE)) {
return fetch(viewName)
@@ -197,7 +197,7 @@ export async function fetchView(
)
}
- let rows
+ let rows: Row[] = []
if (!calculation) {
response.rows = response.rows.map(row => row.doc)
let table: Table
diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts
index 6a037a4ade..51e418c324 100644
--- a/packages/server/src/sdk/app/rows/utils.ts
+++ b/packages/server/src/sdk/app/rows/utils.ts
@@ -1,4 +1,6 @@
-import { TableSchema } from "@budibase/types"
+import cloneDeep from "lodash/cloneDeep"
+import validateJs from "validate.js"
+import { FieldType, Row, Table, TableSchema } from "@budibase/types"
import { FieldTypes } from "../../../constants"
import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters"
@@ -46,3 +48,90 @@ export function cleanExportRows(
return cleanRows
}
+
+function isForeignKey(key: string, table: Table) {
+ const relationships = Object.values(table.schema).filter(
+ column => column.type === FieldType.LINK
+ )
+ return relationships.some(relationship => relationship.foreignKey === key)
+}
+
+export async function validate({
+ tableId,
+ row,
+ table,
+}: {
+ tableId?: string
+ row: Row
+ table?: Table
+}): Promise<{
+ valid: boolean
+ errors: Record
+}> {
+ let fetchedTable: Table
+ if (!table) {
+ fetchedTable = await sdk.tables.getTable(tableId)
+ } else {
+ fetchedTable = table
+ }
+ const errors: Record = {}
+ for (let fieldName of Object.keys(fetchedTable.schema)) {
+ const column = fetchedTable.schema[fieldName]
+ const constraints = cloneDeep(column.constraints)
+ const type = column.type
+ // foreign keys are likely to be enriched
+ if (isForeignKey(fieldName, fetchedTable)) {
+ continue
+ }
+ // formulas shouldn't validated, data will be deleted anyway
+ if (type === FieldTypes.FORMULA || column.autocolumn) {
+ continue
+ }
+ // special case for options, need to always allow unselected (empty)
+ if (type === FieldTypes.OPTIONS && constraints?.inclusion) {
+ constraints.inclusion.push(null as any, "")
+ }
+ let res
+
+ // Validate.js doesn't seem to handle array
+ if (type === FieldTypes.ARRAY && row[fieldName]) {
+ if (row[fieldName].length) {
+ if (!Array.isArray(row[fieldName])) {
+ row[fieldName] = row[fieldName].split(",")
+ }
+ row[fieldName].map((val: any) => {
+ if (
+ !constraints?.inclusion?.includes(val) &&
+ constraints?.inclusion?.length !== 0
+ ) {
+ errors[fieldName] = "Field not in list"
+ }
+ })
+ } else if (constraints?.presence && row[fieldName].length === 0) {
+ // non required MultiSelect creates an empty array, which should not throw errors
+ errors[fieldName] = [`${fieldName} is required`]
+ }
+ } else if (
+ (type === FieldTypes.ATTACHMENT || type === FieldTypes.JSON) &&
+ typeof row[fieldName] === "string"
+ ) {
+ // this should only happen if there is an error
+ try {
+ const json = JSON.parse(row[fieldName])
+ if (type === FieldTypes.ATTACHMENT) {
+ if (Array.isArray(json)) {
+ row[fieldName] = json
+ } else {
+ errors[fieldName] = [`Must be an array`]
+ }
+ }
+ } catch (err) {
+ errors[fieldName] = [`Contains invalid JSON`]
+ }
+ } else {
+ res = validateJs.single(row[fieldName], constraints)
+ }
+ if (res) errors[fieldName] = res
+ }
+ return { valid: Object.keys(errors).length === 0, errors }
+}
diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts
index cd9f42b82c..a6002a72d8 100644
--- a/packages/server/src/tests/utilities/api/index.ts
+++ b/packages/server/src/tests/utilities/api/index.ts
@@ -1,13 +1,16 @@
import TestConfiguration from "../TestConfiguration"
+import { RowAPI } from "./row"
import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2"
export default class API {
table: TableAPI
viewV2: ViewV2API
+ row: RowAPI
constructor(config: TestConfiguration) {
this.table = new TableAPI(config)
this.viewV2 = new ViewV2API(config)
+ this.row = new RowAPI(config)
}
}
diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts
new file mode 100644
index 0000000000..9c7e33278d
--- /dev/null
+++ b/packages/server/src/tests/utilities/api/row.ts
@@ -0,0 +1,22 @@
+import { PatchRowRequest } from "@budibase/types"
+import TestConfiguration from "../TestConfiguration"
+import { TestAPI } from "./base"
+
+export class RowAPI extends TestAPI {
+ constructor(config: TestConfiguration) {
+ super(config)
+ }
+
+ patch = async (
+ tableId: string,
+ row: PatchRowRequest,
+ { expectStatus } = { expectStatus: 200 }
+ ) => {
+ return this.request
+ .patch(`/api/${tableId}/rows`)
+ .send(row)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ }
+}
diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts
index 4b6e0f6e87..8e95a15dca 100644
--- a/packages/server/src/utilities/rowProcessor/index.ts
+++ b/packages/server/src/utilities/rowProcessor/index.ts
@@ -186,18 +186,21 @@ export function inputProcessing(
* @param {object} opts used to set some options for the output, such as disabling relationship squashing.
* @returns {object[]|object} the enriched rows will be returned.
*/
-export async function outputProcessing(
+export async function outputProcessing(
table: Table,
- rows: Row[] | Row,
+ rows: T,
opts = { squash: true }
-) {
+): Promise {
+ let safeRows: Row[]
let wasArray = true
if (!(rows instanceof Array)) {
- rows = [rows]
+ safeRows = [rows]
wasArray = false
+ } else {
+ safeRows = rows
}
// attach any linked row information
- let enriched = await linkRows.attachFullLinkedDocs(table, rows as Row[])
+ let enriched = await linkRows.attachFullLinkedDocs(table, safeRows)
// process formulas
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
@@ -221,7 +224,7 @@ export async function outputProcessing(
enriched
)) as Row[]
}
- return wasArray ? enriched : enriched[0]
+ return (wasArray ? enriched : enriched[0]) as T
}
/**
diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts
index d40d2ee15d..fedb8ec146 100644
--- a/packages/types/src/api/web/app/rows.ts
+++ b/packages/types/src/api/web/app/rows.ts
@@ -1,3 +1,13 @@
+import { Row } from "../../../documents"
+
+export interface PatchRowRequest extends Row {
+ _id: string
+ _rev: string
+ tableId: string
+}
+
+export interface PatchRowResponse extends Row {}
+
export interface SearchResponse {
rows: any[]
}
diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts
index a2295c4a42..c09dc79b95 100644
--- a/packages/types/src/documents/app/row.ts
+++ b/packages/types/src/documents/app/row.ts
@@ -30,5 +30,6 @@ export interface RowAttachment {
export interface Row extends Document {
type?: string
tableId?: string
+ _viewId?: string
[key: string]: any
}