Merge branch 'develop' of github.com:Budibase/budibase into views-v2-frontend

This commit is contained in:
Andrew Kingston 2023-07-28 12:56:32 +01:00
commit 5c5015bbc3
15 changed files with 225 additions and 78 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.29-alpha.0", "version": "2.8.29-alpha.1",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -8,26 +8,13 @@ import {
Datasource, Datasource,
IncludeRelationship, IncludeRelationship,
Operation, Operation,
PatchRowRequest,
PatchRowResponse,
Row, Row,
Table, Table,
UserCtx, UserCtx,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" 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( export async function handleRequest(
operation: Operation, operation: Operation,
@ -55,14 +42,12 @@ export async function handleRequest(
) )
} }
export async function patch(ctx: UserCtx) { export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const inputs = ctx.request.body
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
const id = inputs._id const { id, ...rowData } = ctx.request.body
// don't save the ID to db
delete inputs._id const validateResult = await sdk.rows.utils.validate({
const validateResult = await utils.validate({ row: rowData,
row: inputs,
tableId, tableId,
}) })
if (!validateResult.valid) { if (!validateResult.valid) {
@ -70,9 +55,11 @@ export async function patch(ctx: UserCtx) {
} }
const response = await handleRequest(Operation.UPDATE, tableId, { const response = await handleRequest(Operation.UPDATE, tableId, {
id: breakRowIdField(id), 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) const table = await sdk.tables.getTable(tableId)
return { return {
...response, ...response,
@ -84,7 +71,7 @@ export async function patch(ctx: UserCtx) {
export async function save(ctx: UserCtx) { export async function save(ctx: UserCtx) {
const inputs = ctx.request.body const inputs = ctx.request.body
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
const validateResult = await utils.validate({ const validateResult = await sdk.rows.utils.validate({
row: inputs, row: inputs,
tableId, tableId,
}) })
@ -97,7 +84,9 @@ export async function save(ctx: UserCtx) {
const responseRow = response as { row: Row } const responseRow = response as { row: Row }
const rowId = responseRow.row._id const rowId = responseRow.row._id
if (rowId) { if (rowId) {
const row = await getRow(tableId, rowId, { relationships: true }) const row = await sdk.rows.external.getRow(tableId, rowId, {
relationships: true,
})
return { return {
...response, ...response,
row, row,
@ -110,7 +99,7 @@ export async function save(ctx: UserCtx) {
export async function find(ctx: UserCtx) { export async function find(ctx: UserCtx) {
const id = ctx.params.rowId const id = ctx.params.rowId
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
return getRow(tableId, id) return sdk.rows.external.getRow(tableId, id)
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx) {

View File

@ -9,6 +9,8 @@ import {
DeleteRow, DeleteRow,
DeleteRows, DeleteRows,
Row, Row,
PatchRowRequest,
PatchRowResponse,
SearchResponse, SearchResponse,
SortOrder, SortOrder,
SortType, SortType,
@ -29,7 +31,9 @@ function pickApi(tableId: any) {
return internal return internal
} }
export async function patch(ctx: any): Promise<any> { export async function patch(
ctx: UserCtx<PatchRowRequest, PatchRowResponse>
): Promise<any> {
const appId = ctx.appId const appId = ctx.appId
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const body = ctx.request.body const body = ctx.request.body
@ -38,7 +42,7 @@ export async function patch(ctx: any): Promise<any> {
return save(ctx) return save(ctx)
} }
try { try {
const { row, table } = await quotas.addQuery<any>( const { row, table } = await quotas.addQuery(
() => pickApi(tableId).patch(ctx), () => pickApi(tableId).patch(ctx),
{ {
datasourceId: tableId, datasourceId: tableId,
@ -53,7 +57,7 @@ export async function patch(ctx: any): Promise<any> {
ctx.message = `${table.name} updated successfully.` ctx.message = `${table.name} updated successfully.`
ctx.body = row ctx.body = row
gridSocket?.emitRowUpdate(ctx, row) gridSocket?.emitRowUpdate(ctx, row)
} catch (err) { } catch (err: any) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }
@ -78,6 +82,7 @@ export const save = async (ctx: any) => {
ctx.body = row || squashed ctx.body = row || squashed
gridSocket?.emitRowUpdate(ctx, row || squashed) gridSocket?.emitRowUpdate(ctx, row || squashed)
} }
export async function fetchView(ctx: any) { export async function fetchView(ctx: any) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const viewName = decodeURIComponent(ctx.params.viewName) const viewName = decodeURIComponent(ctx.params.viewName)
@ -267,7 +272,7 @@ export async function searchView(ctx: Ctx<void, SearchResponse>) {
undefined undefined
ctx.status = 200 ctx.status = 200
ctx.body = await quotas.addQuery( const result = await quotas.addQuery(
() => () =>
sdk.rows.search({ sdk.rows.search({
tableId: view.tableId, tableId: view.tableId,
@ -279,6 +284,9 @@ export async function searchView(ctx: Ctx<void, SearchResponse>) {
datasourceId: view.tableId, datasourceId: view.tableId,
} }
) )
result.rows.forEach(r => (r._viewId = view.id))
ctx.body = result
} }
export async function validate(ctx: Ctx) { export async function validate(ctx: Ctx) {
@ -287,7 +295,7 @@ export async function validate(ctx: Ctx) {
if (isExternalTable(tableId)) { if (isExternalTable(tableId)) {
ctx.body = { valid: true } ctx.body = { valid: true }
} else { } else {
ctx.body = await utils.validate({ ctx.body = await sdk.rows.utils.validate({
row: ctx.request.body, row: ctx.request.body,
tableId, tableId,
}) })

View File

@ -15,19 +15,26 @@ import * as utils from "./utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { context, db as dbCore } from "@budibase/backend-core" import { context, db as dbCore } from "@budibase/backend-core"
import { finaliseRow, updateRelatedFormula } from "./staticFormula" 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" import sdk from "../../../sdk"
export async function patch(ctx: UserCtx) { export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const inputs = ctx.request.body const inputs = ctx.request.body
const tableId = inputs.tableId const tableId = inputs.tableId
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 {
let dbTable = await sdk.tables.getTable(tableId)
oldRow = await outputProcessing( oldRow = await outputProcessing(
dbTable, dbTable,
await utils.findRow(ctx, tableId, inputs._id) await utils.findRow(ctx, tableId, inputs._id!)
) )
} catch (err) { } catch (err) {
if (isUserTable) { if (isUserTable) {
@ -40,7 +47,7 @@ export async function patch(ctx: UserCtx) {
throw "Row does not exist" throw "Row does not exist"
} }
} }
let dbTable = await sdk.tables.getTable(tableId)
// 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)) {
@ -53,7 +60,7 @@ export async function patch(ctx: UserCtx) {
// 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 } = inputProcessing(ctx.user, tableClone, combinedRow) let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow)
const validateResult = await utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
table, table,
}) })
@ -74,7 +81,7 @@ export async function patch(ctx: UserCtx) {
if (isUserTable) { if (isUserTable) {
// the row has been updated, need to put it into the ctx // 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) await userController.updateMetadata(ctx)
return { row: ctx.body as Row, table } 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) let { table, row } = inputProcessing(ctx.user, tableClone, inputs)
const validateResult = await utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
table, table,
}) })

View File

@ -1,6 +1,5 @@
import { generateUserMetadataID, generateUserFlagID } from "../../db/utils" import { generateUserFlagID } from "../../db/utils"
import { InternalTables } from "../../db/utils" import { InternalTables } from "../../db/utils"
import { getGlobalUsers } from "../../utilities/global"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { Ctx, UserCtx } from "@budibase/types" import { Ctx, UserCtx } from "@budibase/types"

View File

@ -5,7 +5,7 @@ tk.freeze(timestamp)
import { outputProcessing } from "../../../utilities/rowProcessor" import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities" import * as setup from "./utilities"
const { basicRow } = setup.structures 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 { quotas } from "@budibase/pro"
import { import {
QuotaUsageType, QuotaUsageType,
@ -16,6 +16,7 @@ import {
FieldType, FieldType,
SortType, SortType,
SortOrder, SortOrder,
PatchRowRequest,
} from "@budibase/types" } from "@budibase/types"
import { import {
expectAnyInternalColsAttributes, expectAnyInternalColsAttributes,
@ -399,17 +400,12 @@ describe("/rows", () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
const res = await request const res = await config.api.row.patch(table._id!, {
.patch(`/api/${table._id}/rows`) _id: existing._id!,
.send({ _rev: existing._rev!,
_id: existing._id, tableId: table._id!,
_rev: existing._rev,
tableId: table._id,
name: "Updated Name", name: "Updated Name",
}) })
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect((res as any).res.statusMessage).toEqual( expect((res as any).res.statusMessage).toEqual(
`${table.name} updated successfully.` `${table.name} updated successfully.`
@ -430,16 +426,16 @@ describe("/rows", () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
await request await config.api.row.patch(
.patch(`/api/${table._id}/rows`) table._id!,
.send({ {
_id: existing._id, _id: existing._id!,
_rev: existing._rev, _rev: existing._rev!,
tableId: table._id, tableId: table._id!,
name: 1, name: 1,
}) },
.set(config.defaultHeaders()) { expectStatus: 400 }
.expect(400) )
await assertRowUsage(rowUsage) await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage) 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 } }, 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).toHaveLength(10)
expect(response.body.rows).toEqual( expect(response.body.rows).toEqual(
expect.arrayContaining( expect.arrayContaining(
rows.map(r => ({ rows.map(r => ({
...expectAnyInternalColsAttributes, ...expectAnyInternalColsAttributes,
_viewId: view.id,
name: r.name, name: r.name,
})) }))
) )

View File

@ -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
}

View File

@ -2,10 +2,12 @@ import * as attachments from "./attachments"
import * as rows from "./rows" import * as rows from "./rows"
import * as search from "./search" import * as search from "./search"
import * as utils from "./utils" import * as utils from "./utils"
import * as external from "./external"
export default { export default {
...attachments, ...attachments,
...rows, ...rows,
...search, ...search,
utils: utils, utils,
external,
} }

View File

@ -147,8 +147,8 @@ export async function exportRows(
export async function fetch(tableId: string) { export async function fetch(tableId: string) {
const db = context.getAppDB() const db = context.getAppDB()
let table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
let rows = await getRawTableData(db, tableId) const rows = await getRawTableData(db, tableId)
const result = await outputProcessing(table, rows) const result = await outputProcessing(table, rows)
return result return result
} }
@ -171,7 +171,7 @@ async function getRawTableData(db: Database, tableId: string) {
export async function fetchView( export async function fetchView(
viewName: string, viewName: string,
options: { calculation: string; group: string; field: string } options: { calculation: string; group: string; field: string }
) { ): Promise<Row[]> {
// if this is a table view being looked for just transfer to that // if this is a table view being looked for just transfer to that
if (viewName.startsWith(DocumentType.TABLE)) { if (viewName.startsWith(DocumentType.TABLE)) {
return fetch(viewName) return fetch(viewName)
@ -197,7 +197,7 @@ export async function fetchView(
) )
} }
let rows let rows: Row[] = []
if (!calculation) { if (!calculation) {
response.rows = response.rows.map(row => row.doc) response.rows = response.rows.map(row => row.doc)
let table: Table let table: Table

View File

@ -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 { FieldTypes } from "../../../constants"
import { makeExternalQuery } from "../../../integrations/base/query" import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
@ -46,3 +48,90 @@ export function cleanExportRows(
return cleanRows 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<string, any>
}> {
let fetchedTable: Table
if (!table) {
fetchedTable = await sdk.tables.getTable(tableId)
} else {
fetchedTable = table
}
const errors: Record<string, any> = {}
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 }
}

View File

@ -1,13 +1,16 @@
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { RowAPI } from "./row"
import { TableAPI } from "./table" import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2" import { ViewV2API } from "./viewV2"
export default class API { export default class API {
table: TableAPI table: TableAPI
viewV2: ViewV2API viewV2: ViewV2API
row: RowAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
this.viewV2 = new ViewV2API(config) this.viewV2 = new ViewV2API(config)
this.row = new RowAPI(config)
} }
} }

View File

@ -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)
}
}

View File

@ -186,18 +186,21 @@ export function inputProcessing(
* @param {object} opts used to set some options for the output, such as disabling relationship squashing. * @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. * @returns {object[]|object} the enriched rows will be returned.
*/ */
export async function outputProcessing( export async function outputProcessing<T extends Row[] | Row>(
table: Table, table: Table,
rows: Row[] | Row, rows: T,
opts = { squash: true } opts = { squash: true }
) { ): Promise<T> {
let safeRows: Row[]
let wasArray = true let wasArray = true
if (!(rows instanceof Array)) { if (!(rows instanceof Array)) {
rows = [rows] safeRows = [rows]
wasArray = false wasArray = false
} else {
safeRows = rows
} }
// attach any linked row information // attach any linked row information
let enriched = await linkRows.attachFullLinkedDocs(table, rows as Row[]) let enriched = await linkRows.attachFullLinkedDocs(table, safeRows)
// process formulas // process formulas
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[] enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
@ -221,7 +224,7 @@ export async function outputProcessing(
enriched enriched
)) as Row[] )) as Row[]
} }
return wasArray ? enriched : enriched[0] return (wasArray ? enriched : enriched[0]) as T
} }
/** /**

View File

@ -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 { export interface SearchResponse {
rows: any[] rows: any[]
} }

View File

@ -30,5 +30,6 @@ export interface RowAttachment {
export interface Row extends Document { export interface Row extends Document {
type?: string type?: string
tableId?: string tableId?: string
_viewId?: string
[key: string]: any [key: string]: any
} }