2024-03-20 19:33:39 +01:00
|
|
|
import {
|
|
|
|
FieldType,
|
2024-04-26 12:23:11 +02:00
|
|
|
BBReferenceFieldSubType,
|
2024-03-20 19:33:39 +01:00
|
|
|
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"
|
2023-01-17 16:07:52 +01:00
|
|
|
|
|
|
|
type Rows = Array<Row>
|
|
|
|
|
|
|
|
interface SchemaValidation {
|
|
|
|
[index: string]: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ValidationResults {
|
|
|
|
schemaValidation: SchemaValidation
|
|
|
|
allValid: boolean
|
|
|
|
invalidColumns: Array<string>
|
2023-07-20 13:21:09 +02:00
|
|
|
errors: Record<string, string>
|
2023-01-17 16:07:52 +01:00
|
|
|
}
|
|
|
|
|
2024-03-20 19:33:39 +01:00
|
|
|
export function isSchema(schema: any): schema is TableSchema {
|
2023-01-17 16:07:52 +01:00
|
|
|
return (
|
|
|
|
typeof schema === "object" &&
|
2024-03-20 19:33:39 +01:00
|
|
|
Object.values<FieldSchema>(schema).every(column => {
|
2023-01-17 16:07:52 +01:00
|
|
|
return (
|
|
|
|
column !== null &&
|
|
|
|
typeof column === "object" &&
|
|
|
|
typeof column.type === "string" &&
|
2024-01-24 17:58:13 +01:00
|
|
|
Object.values(FieldType).includes(column.type as FieldType)
|
2023-01-17 16:07:52 +01:00
|
|
|
)
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-01-17 16:07:52 +01:00
|
|
|
const results: ValidationResults = {
|
|
|
|
schemaValidation: {},
|
|
|
|
allValid: false,
|
|
|
|
invalidColumns: [],
|
2023-07-20 13:21:09 +02:00
|
|
|
errors: {},
|
2023-01-17 16:07:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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-05-02 13:19:19 +02:00
|
|
|
constraints,
|
2024-04-03 17:01:36 +02:00
|
|
|
} = schema[columnName] || {}
|
2023-01-17 16:07:52 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-05-02 13:19:19 +02:00
|
|
|
const isRequired =
|
|
|
|
!!constraints &&
|
|
|
|
((typeof constraints.presence !== "boolean" &&
|
|
|
|
!constraints.presence?.allowEmpty) ||
|
|
|
|
constraints.presence === true)
|
|
|
|
|
2023-01-17 16:07:52 +01:00
|
|
|
// 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)
|
2023-07-20 13:21:09 +02:00
|
|
|
} else if (!columnName.match(ValidColumnNameRegex)) {
|
|
|
|
// Check for special characters in column names
|
2023-07-20 15:06:31 +02:00
|
|
|
results.schemaValidation[columnName] = false
|
2023-07-20 13:21:09 +02:00
|
|
|
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
|
2023-01-17 16:07:52 +01:00
|
|
|
} 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
|
2024-01-24 17:58:13 +01:00
|
|
|
} else if (columnType === FieldType.NUMBER && isNaN(Number(columnData))) {
|
2023-01-17 16:07:52 +01:00
|
|
|
// If provided must be a valid number
|
|
|
|
results.schemaValidation[columnName] = false
|
|
|
|
} else if (
|
|
|
|
// If provided must be a valid date
|
2024-01-24 17:58:13 +01:00
|
|
|
columnType === FieldType.DATETIME &&
|
2023-01-17 16:07:52 +01:00
|
|
|
isNaN(new Date(columnData).getTime())
|
|
|
|
) {
|
|
|
|
results.schemaValidation[columnName] = false
|
2023-10-09 18:02:21 +02:00
|
|
|
} else if (
|
2024-04-22 11:14:23 +02:00
|
|
|
(columnType === FieldType.BB_REFERENCE ||
|
|
|
|
columnType === FieldType.BB_REFERENCE_SINGLE) &&
|
2024-05-02 13:19:19 +02:00
|
|
|
!isValidBBReference(columnData, columnType, columnSubtype, isRequired)
|
2023-10-09 18:02:21 +02:00
|
|
|
) {
|
|
|
|
results.schemaValidation[columnName] = false
|
2023-01-17 16:07:52 +01:00
|
|
|
} 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 {
|
2023-01-17 16:07:52 +01:00
|
|
|
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-05-20 16:34:22 +02:00
|
|
|
const columnSchema = schema[columnName]
|
|
|
|
const { type: columnType } = columnSchema
|
2024-01-24 17:58:13 +01:00
|
|
|
if (columnType === FieldType.NUMBER) {
|
2023-01-17 16:07:52 +01:00
|
|
|
// If provided must be a valid number
|
|
|
|
parsedRow[columnName] = columnData ? Number(columnData) : columnData
|
2024-05-20 16:34:22 +02:00
|
|
|
} else if (columnType === FieldType.DATETIME && !columnSchema.timeOnly) {
|
2023-01-17 16:07:52 +01:00
|
|
|
// If provided must be a valid date
|
|
|
|
parsedRow[columnName] = columnData
|
|
|
|
? new Date(columnData).toISOString()
|
|
|
|
: columnData
|
2024-01-24 17:58:13 +01:00
|
|
|
} else if (columnType === FieldType.BB_REFERENCE) {
|
2024-05-10 11:45:15 +02:00
|
|
|
let parsedValues: { _id: string }[] = columnData || []
|
|
|
|
if (columnData) {
|
|
|
|
parsedValues = parseCsvExport<{ _id: string }[]>(columnData)
|
|
|
|
}
|
2024-05-03 17:53:59 +02:00
|
|
|
|
|
|
|
parsedRow[columnName] = parsedValues?.map(u => u._id)
|
2024-04-25 16:31:15 +02:00
|
|
|
} else if (columnType === FieldType.BB_REFERENCE_SINGLE) {
|
|
|
|
const parsedValue =
|
|
|
|
columnData && parseCsvExport<{ _id: string }>(columnData)
|
|
|
|
parsedRow[columnName] = parsedValue?._id
|
2024-04-03 17:01:36 +02:00
|
|
|
} else if (
|
|
|
|
(columnType === FieldType.ATTACHMENTS ||
|
2024-05-13 17:19:02 +02:00
|
|
|
columnType === FieldType.ATTACHMENT_SINGLE ||
|
2024-05-17 18:04:15 +02:00
|
|
|
columnType === FieldType.SIGNATURE_SINGLE) &&
|
2024-04-03 17:01:36 +02:00
|
|
|
typeof columnData === "string"
|
|
|
|
) {
|
|
|
|
parsedRow[columnName] = parseCsvExport(columnData)
|
2023-01-17 16:07:52 +01:00
|
|
|
} else {
|
|
|
|
parsedRow[columnName] = columnData
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return parsedRow
|
|
|
|
})
|
|
|
|
}
|
2023-10-09 18:02:21 +02:00
|
|
|
|
|
|
|
function isValidBBReference(
|
2024-04-25 15:50:28 +02:00
|
|
|
data: any,
|
|
|
|
type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE,
|
2024-05-02 13:19:19 +02:00
|
|
|
subtype: BBReferenceFieldSubType,
|
|
|
|
isRequired: boolean
|
2023-10-09 18:02:21 +02:00
|
|
|
): boolean {
|
2024-04-25 15:50:28 +02:00
|
|
|
if (typeof data !== "string") {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === FieldType.BB_REFERENCE_SINGLE) {
|
|
|
|
if (!data) {
|
2024-05-02 13:19:19 +02:00
|
|
|
return !isRequired
|
2024-04-25 15:50:28 +02:00
|
|
|
}
|
|
|
|
const user = parseCsvExport<{ _id: string }>(data)
|
|
|
|
return db.isGlobalUserID(user._id)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (subtype) {
|
2024-04-26 12:23:11 +02:00
|
|
|
case BBReferenceFieldSubType.USER:
|
|
|
|
case BBReferenceFieldSubType.USERS: {
|
2024-04-25 15:50:28 +02:00
|
|
|
const userArray = parseCsvExport<{ _id: string }[]>(data)
|
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: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:
|
2024-04-25 15:50:28 +02:00
|
|
|
throw utils.unreachable(subtype)
|
2023-10-09 18:02:21 +02:00
|
|
|
}
|
|
|
|
}
|