(data: RowData, schema: TableSchema)
| 212 | |
| 213 | /** Validates row data matches schema column types and required fields. */ |
| 214 | export function validateRowAgainstSchema(data: RowData, schema: TableSchema): ValidationResult { |
| 215 | const errors: string[] = [] |
| 216 | |
| 217 | for (const column of schema.columns) { |
| 218 | const value = data[getColumnId(column)] |
| 219 | |
| 220 | if (column.required && (value === undefined || value === null)) { |
| 221 | errors.push(`Missing required field: ${column.name}`) |
| 222 | continue |
| 223 | } |
| 224 | |
| 225 | if (value === null || value === undefined) continue |
| 226 | |
| 227 | switch (column.type) { |
| 228 | case 'string': |
| 229 | if (typeof value !== 'string') { |
| 230 | errors.push(`${column.name} must be string, got ${typeof value}`) |
| 231 | } else if (value.length > TABLE_LIMITS.MAX_STRING_VALUE_LENGTH) { |
| 232 | errors.push(`${column.name} exceeds max string length`) |
| 233 | } |
| 234 | break |
| 235 | case 'number': |
| 236 | if (typeof value !== 'number' || Number.isNaN(value)) { |
| 237 | errors.push(`${column.name} must be number`) |
| 238 | } |
| 239 | break |
| 240 | case 'boolean': |
| 241 | if (typeof value !== 'boolean') { |
| 242 | errors.push(`${column.name} must be boolean`) |
| 243 | } |
| 244 | break |
| 245 | case 'date': |
| 246 | if ( |
| 247 | !(value instanceof Date) && |
| 248 | (typeof value !== 'string' || Number.isNaN(Date.parse(value))) |
| 249 | ) { |
| 250 | errors.push(`${column.name} must be valid date`) |
| 251 | } |
| 252 | break |
| 253 | case 'json': |
| 254 | try { |
| 255 | JSON.stringify(value) |
| 256 | } catch { |
| 257 | errors.push(`${column.name} must be valid JSON`) |
| 258 | } |
| 259 | break |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | return { valid: errors.length === 0, errors } |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * Attempts to coerce a non-null value to a column's declared type. Returns the |
no test coverage detected