budibase/packages/server/src/sdk/app/rows/utils.ts

207 lines
5.9 KiB
TypeScript
Raw Normal View History

2023-07-26 14:12:58 +02:00
import cloneDeep from "lodash/cloneDeep"
import validateJs from "validate.js"
2024-02-29 13:33:03 +01:00
import {
2024-03-05 18:42:44 +01:00
Datasource,
DatasourcePlusQueryResponse,
2024-02-29 13:33:03 +01:00
FieldType,
QueryJson,
Row,
2024-03-05 18:42:44 +01:00
SourceName,
2024-02-29 13:33:03 +01:00
Table,
TableSchema,
} from "@budibase/types"
2023-07-14 17:04:59 +02:00
import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters"
import sdk from "../.."
2023-10-05 19:47:00 +02:00
import { isRelationshipColumn } from "../../../db/utils"
import { SqlClient, isSQL } from "../../../integrations/utils"
2024-03-05 18:42:44 +01:00
const SQL_CLIENT_SOURCE_MAP: Record<SourceName, SqlClient | undefined> = {
[SourceName.POSTGRES]: SqlClient.POSTGRES,
[SourceName.MYSQL]: SqlClient.MY_SQL,
[SourceName.SQL_SERVER]: SqlClient.MS_SQL,
[SourceName.ORACLE]: SqlClient.ORACLE,
[SourceName.DYNAMODB]: undefined,
[SourceName.MONGODB]: undefined,
[SourceName.ELASTICSEARCH]: undefined,
[SourceName.COUCHDB]: undefined,
[SourceName.S3]: undefined,
[SourceName.AIRTABLE]: undefined,
[SourceName.ARANGODB]: undefined,
[SourceName.REST]: undefined,
[SourceName.FIRESTORE]: undefined,
[SourceName.GOOGLE_SHEETS]: undefined,
[SourceName.REDIS]: undefined,
[SourceName.SNOWFLAKE]: undefined,
[SourceName.BUDIBASE]: undefined,
}
export function getSQLClient(datasource: Datasource): SqlClient {
if (!isSQL(datasource)) {
throw new Error("Cannot get SQL Client for non-SQL datasource")
}
2024-03-05 18:42:44 +01:00
const lookup = SQL_CLIENT_SOURCE_MAP[datasource.source]
if (lookup) {
return lookup
}
2024-03-05 18:42:44 +01:00
throw new Error("Unable to determine client for SQL datasource")
}
2023-07-14 17:04:59 +02:00
2024-02-29 13:33:03 +01:00
export async function getDatasourceAndQuery(
json: QueryJson
): Promise<DatasourcePlusQueryResponse> {
2023-07-14 17:04:59 +02:00
const datasourceId = json.endpoint.datasourceId
const datasource = await sdk.datasources.get(datasourceId)
const table = datasource.entities?.[json.endpoint.entityId]
if (!json.meta && table) {
json.meta = {
table,
}
}
2023-07-14 17:04:59 +02:00
return makeExternalQuery(datasource, json)
}
export function cleanExportRows(
rows: any[],
schema: TableSchema,
2023-07-14 17:04:59 +02:00
format: string,
columns?: string[],
customHeaders: { [key: string]: string } = {}
2023-07-14 17:04:59 +02:00
) {
let cleanRows = [...rows]
const relationships = Object.entries(schema)
.filter((entry: any[]) => entry[1].type === FieldType.LINK)
2023-07-14 17:04:59 +02:00
.map(entry => entry[0])
relationships.forEach(column => {
cleanRows.forEach(row => {
delete row[column]
})
delete schema[column]
})
if (format === Format.CSV) {
// Intended to append empty values in export
const schemaKeys = Object.keys(schema)
for (let key of schemaKeys) {
if (columns?.length && columns.indexOf(key) > 0) {
continue
}
for (let row of cleanRows) {
if (row[key] == null) {
row[key] = undefined
}
}
}
} else if (format === Format.JSON) {
// Replace row keys with custom headers
for (let row of cleanRows) {
renameKeys(customHeaders, row)
}
2023-07-14 17:04:59 +02:00
}
return cleanRows
}
2023-07-26 14:12:58 +02:00
function renameKeys(keysMap: { [key: string]: any }, row: any) {
for (const key in keysMap) {
Object.defineProperty(
row,
keysMap[key],
Object.getOwnPropertyDescriptor(row, key) || {}
)
delete row[key]
}
}
2023-07-26 14:12:58 +02:00
function isForeignKey(key: string, table: Table) {
2023-10-05 19:47:00 +02:00
const relationships = Object.values(table.schema).filter(isRelationshipColumn)
return relationships.some(
relationship => (relationship as any).foreignKey === key
2023-07-26 14:12:58 +02:00
)
}
export async function validate({
tableId,
row,
table,
}: {
tableId?: string
row: Row
table?: Table
}): Promise<{
valid: boolean
errors: Record<string, any>
}> {
let fetchedTable: Table | undefined
if (!table && tableId) {
2023-07-26 14:12:58 +02:00
fetchedTable = await sdk.tables.getTable(tableId)
} else if (table) {
2023-07-26 14:12:58 +02:00
fetchedTable = table
}
if (fetchedTable === undefined) {
throw new Error("Unable to fetch table for validation")
}
2023-07-26 14:12:58 +02:00
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 === FieldType.FORMULA || column.autocolumn) {
2023-07-26 14:12:58 +02:00
continue
}
// special case for options, need to always allow unselected (empty)
if (type === FieldType.OPTIONS && constraints?.inclusion) {
2023-07-26 14:12:58 +02:00
constraints.inclusion.push(null as any, "")
}
let res
// Validate.js doesn't seem to handle array
if (type === FieldType.ARRAY && row[fieldName]) {
2023-07-26 14:12:58 +02:00
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 === FieldType.ATTACHMENTS || type === FieldType.JSON) &&
2023-07-26 14:12:58 +02:00
typeof row[fieldName] === "string"
) {
// this should only happen if there is an error
try {
const json = JSON.parse(row[fieldName])
if (type === FieldType.ATTACHMENTS) {
2023-07-26 14:12:58 +02:00
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 }
}