From 11f05694469dda348364cb0d560e31c47d8563c8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 14:12:58 +0200 Subject: [PATCH] Move row.validate to the sdk --- .../src/api/controllers/row/external.ts | 5 +- .../src/api/controllers/row/internal.ts | 8 +- packages/server/src/sdk/app/rows/utils.ts | 91 ++++++++++++++++++- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 0b6471eefd..aac94707e6 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -15,7 +15,6 @@ import { UserCtx, } from "@budibase/types" import sdk from "../../../sdk" -import * as utils from "./utils" async function getRow( tableId: string, @@ -61,7 +60,7 @@ export async function patch(ctx: UserCtx) { const tableId = ctx.params.tableId const { id, ...rowData } = ctx.request.body - const validateResult = await utils.validate({ + const validateResult = await sdk.rows.utils.validate({ row: rowData, tableId, }) @@ -84,7 +83,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, }) diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index cdf0a396dc..1153461b89 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -30,8 +30,8 @@ export async function patch(ctx: UserCtx) { 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!) @@ -47,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)) { @@ -60,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, }) @@ -109,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/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 } +}