2023-01-17 16:07:52 +01:00
|
|
|
import { FieldTypes } from "../constants"
|
2023-07-20 13:21:09 +02:00
|
|
|
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
2023-01-17 16:07:52 +01:00
|
|
|
|
|
|
|
interface SchemaColumn {
|
|
|
|
readonly name: string
|
|
|
|
readonly type: FieldTypes
|
|
|
|
readonly autocolumn?: boolean
|
2023-05-02 13:48:05 +02:00
|
|
|
readonly constraints?: {
|
|
|
|
presence: boolean
|
|
|
|
}
|
2023-01-17 16:07:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Schema {
|
|
|
|
readonly [index: string]: SchemaColumn
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Row {
|
|
|
|
[index: string]: any
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
const PARSERS: any = {
|
|
|
|
[FieldTypes.NUMBER]: (attribute?: string) => {
|
|
|
|
if (!attribute) {
|
|
|
|
return attribute
|
|
|
|
}
|
|
|
|
return Number(attribute)
|
|
|
|
},
|
|
|
|
[FieldTypes.DATETIME]: (attribute?: string) => {
|
|
|
|
if (!attribute) {
|
|
|
|
return attribute
|
|
|
|
}
|
|
|
|
return new Date(attribute).toISOString()
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isSchema(schema: any): schema is Schema {
|
|
|
|
return (
|
|
|
|
typeof schema === "object" &&
|
|
|
|
Object.values(schema).every(rawColumn => {
|
|
|
|
const column = rawColumn as SchemaColumn
|
|
|
|
|
|
|
|
return (
|
|
|
|
column !== null &&
|
|
|
|
typeof column === "object" &&
|
|
|
|
typeof column.type === "string" &&
|
|
|
|
Object.values(FieldTypes).includes(column.type as FieldTypes)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isRows(rows: any): rows is Rows {
|
|
|
|
return Array.isArray(rows) && rows.every(row => typeof row === "object")
|
|
|
|
}
|
|
|
|
|
|
|
|
export function validate(rows: Rows, schema: Schema): ValidationResults {
|
|
|
|
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]) => {
|
|
|
|
const columnType = schema[columnName]?.type
|
|
|
|
const isAutoColumn = schema[columnName]?.autocolumn
|
|
|
|
|
|
|
|
// 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
|
|
|
|
results.invalidColumns.push(columnName)
|
|
|
|
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
|
|
|
|
} else if (
|
|
|
|
columnType === FieldTypes.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 === FieldTypes.DATETIME &&
|
|
|
|
isNaN(new Date(columnData).getTime())
|
|
|
|
) {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
export function parse(rows: Rows, schema: Schema): 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
|
|
|
|
}
|
|
|
|
|
|
|
|
const columnType = schema[columnName].type
|
|
|
|
|
|
|
|
if (columnType === FieldTypes.NUMBER) {
|
|
|
|
// If provided must be a valid number
|
|
|
|
parsedRow[columnName] = columnData ? Number(columnData) : columnData
|
|
|
|
} else if (columnType === FieldTypes.DATETIME) {
|
|
|
|
// If provided must be a valid date
|
|
|
|
parsedRow[columnName] = columnData
|
|
|
|
? new Date(columnData).toISOString()
|
|
|
|
: columnData
|
|
|
|
} else {
|
|
|
|
parsedRow[columnName] = columnData
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return parsedRow
|
|
|
|
})
|
|
|
|
}
|