budibase/packages/server/src/utilities/schema.ts

193 lines
5.9 KiB
TypeScript
Raw Normal View History

2024-03-20 19:33:39 +01:00
import {
FieldType,
FieldSubtype,
TableSchema,
FieldSchema,
Row,
} from "@budibase/types"
2023-10-09 18:02:21 +02:00
import { ValidColumnNameRegex, utils } from "@budibase/shared-core"
import { db } from "@budibase/backend-core"
2023-10-10 10:47:43 +02:00
import { parseCsvExport } from "../api/controllers/view/exporters"
type Rows = Array<Row>
interface SchemaValidation {
[index: string]: boolean
}
interface ValidationResults {
schemaValidation: SchemaValidation
allValid: boolean
invalidColumns: Array<string>
errors: Record<string, string>
}
2024-03-20 19:33:39 +01:00
export function isSchema(schema: any): schema is TableSchema {
return (
typeof schema === "object" &&
2024-03-20 19:33:39 +01:00
Object.values<FieldSchema>(schema).every(column => {
return (
column !== null &&
typeof column === "object" &&
typeof column.type === "string" &&
Object.values(FieldType).includes(column.type as FieldType)
)
})
)
}
export function isRows(rows: any): rows is Rows {
return Array.isArray(rows) && rows.every(row => typeof row === "object")
}
2024-03-20 19:33:39 +01:00
export function validate(rows: Rows, schema: TableSchema): ValidationResults {
const results: ValidationResults = {
schemaValidation: {},
allValid: false,
invalidColumns: [],
errors: {},
}
rows.forEach(row => {
Object.entries(row).forEach(([columnName, columnData]) => {
2024-03-20 19:33:39 +01:00
const {
type: columnType,
subtype: columnSubtype,
autocolumn: isAutoColumn,
2024-04-03 17:01:36 +02:00
} = schema[columnName] || {}
2023-10-09 18:02:21 +02:00
// If the column had an invalid value we don't want to override it
if (results.schemaValidation[columnName] === false) {
return
}
// If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array
if (typeof columnType !== "string") {
results.invalidColumns.push(columnName)
} else if (!columnName.match(ValidColumnNameRegex)) {
// Check for special characters in column names
results.schemaValidation[columnName] = false
results.errors[columnName] =
"Column names can't contain special characters"
2023-05-02 13:48:05 +02:00
} else if (
columnData == null &&
!schema[columnName].constraints?.presence
) {
results.schemaValidation[columnName] = true
} else if (
// If there's no data for this field don't bother with further checks
// If the field is already marked as invalid there's no need for further checks
results.schemaValidation[columnName] === false ||
columnData == null ||
isAutoColumn
) {
return
} else if (columnType === FieldType.NUMBER && isNaN(Number(columnData))) {
// If provided must be a valid number
results.schemaValidation[columnName] = false
} else if (
// If provided must be a valid date
columnType === FieldType.DATETIME &&
isNaN(new Date(columnData).getTime())
) {
results.schemaValidation[columnName] = false
2023-10-09 18:02:21 +02:00
} else if (
columnType === FieldType.BB_REFERENCE &&
2023-10-09 18:02:21 +02:00
!isValidBBReference(columnData, columnSubtype)
) {
results.schemaValidation[columnName] = false
} else {
results.schemaValidation[columnName] = true
}
})
})
results.allValid =
Object.values(results.schemaValidation).length > 0 &&
Object.values(results.schemaValidation).every(column => column)
// Select unique values
results.invalidColumns = [...new Set(results.invalidColumns)]
return results
}
2024-03-20 19:33:39 +01:00
export function parse(rows: Rows, schema: TableSchema): Rows {
return rows.map(row => {
const parsedRow: Row = {}
Object.entries(row).forEach(([columnName, columnData]) => {
if (!(columnName in schema) || schema[columnName]?.autocolumn) {
// Objects can be present in the row data but not in the schema, so make sure we don't proceed in such a case
return
}
2024-03-20 19:33:39 +01:00
const { type: columnType, subtype: columnSubtype } = schema[columnName]
if (columnType === FieldType.NUMBER) {
// If provided must be a valid number
parsedRow[columnName] = columnData ? Number(columnData) : columnData
} else if (columnType === FieldType.DATETIME) {
// If provided must be a valid date
parsedRow[columnName] = columnData
? new Date(columnData).toISOString()
: columnData
} else if (columnType === FieldType.BB_REFERENCE) {
2023-10-10 13:37:59 +02:00
const parsedValues =
!!columnData && parseCsvExport<{ _id: string }[]>(columnData)
if (!parsedValues) {
parsedRow[columnName] = undefined
} else {
switch (columnSubtype) {
case FieldSubtype.USER:
parsedRow[columnName] = parsedValues[0]?._id
break
case FieldSubtype.USERS:
parsedRow[columnName] = parsedValues.map(u => u._id)
break
default:
utils.unreachable(columnSubtype)
}
}
2024-04-03 17:01:36 +02:00
} else if (
(columnType === FieldType.ATTACHMENTS ||
columnType === FieldType.ATTACHMENT_SINGLE) &&
typeof columnData === "string"
) {
parsedRow[columnName] = parseCsvExport(columnData)
} else {
parsedRow[columnName] = columnData
}
})
return parsedRow
})
}
2023-10-09 18:02:21 +02:00
function isValidBBReference(
columnData: any,
2024-03-20 19:33:39 +01:00
columnSubtype: FieldSubtype.USER | FieldSubtype.USERS
2023-10-09 18:02:21 +02:00
): boolean {
switch (columnSubtype) {
case FieldSubtype.USER:
2024-03-19 16:58:25 +01:00
case FieldSubtype.USERS: {
2023-10-10 10:47:43 +02:00
if (typeof columnData !== "string") {
return false
}
2023-10-10 14:20:26 +02:00
const userArray = parseCsvExport<{ _id: string }[]>(columnData)
2023-10-10 15:39:05 +02:00
if (!Array.isArray(userArray)) {
2023-10-09 18:02:21 +02:00
return false
}
2023-10-10 14:39:55 +02:00
if (columnSubtype === FieldSubtype.USER && userArray.length > 1) {
2023-10-09 18:02:21 +02:00
return false
}
2023-10-10 14:20:26 +02:00
const constainsWrongId = userArray.find(
user => !db.isGlobalUserID(user._id)
)
return !constainsWrongId
2024-03-19 16:58:25 +01:00
}
2023-10-09 18:02:21 +02:00
default:
throw utils.unreachable(columnSubtype)
}
}