UNPKG

@nozbe/watermelondb

Version:

Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast

135 lines (115 loc) 4.79 kB
// @flow /* eslint-disable no-lonely-if */ /* eslint-disable no-self-compare */ import { type ColumnName, type ColumnSchema, type TableSchema } from '../Schema' import { type RecordId, type SyncStatus } from '../Model' import randomId from '../utils/common/randomId' // Raw object representing a model record, coming from an untrusted source // (disk, sync, user data). Before it can be used to create a Model instance // it must be sanitized (with `sanitizedRaw`) into a RawRecord export type DirtyRaw = Object // These fields are ALWAYS present in records of any collection. type _RawRecord = { id: RecordId, _status: SyncStatus, _changed: string, } // Raw object representing a model record. A RawRecord is guaranteed by the type system // to be safe to use (sanitied with `sanitizedRaw`): // - it has exactly the fields described by TableSchema (+ standard fields) // - every field is exactly the type described by ColumnSchema (string, number, or boolean) // - … and the same optionality (will not be null unless isOptional: true) export opaque type RawRecord: _RawRecord = _RawRecord // a number, but not NaN (NaN !== NaN) or Infinity function isValidNumber(value: any): boolean { return typeof value === 'number' && value === value && value !== Infinity && value !== -Infinity } // Note: This is performance-critical code function _setRaw(raw: Object, key: string, value: any, columnSchema: ColumnSchema): void { const { type, isOptional } = columnSchema // If the value is wrong type or invalid, it's set to `null` (if optional) or empty value ('', 0, false) if (type === 'string') { if (typeof value === 'string') { raw[key] = value } else { raw[key] = isOptional ? null : '' } } else if (type === 'boolean') { if (typeof value === 'boolean') { raw[key] = value } else if (value === 1 || value === 0) { // Exception to the standard rule — because SQLite turns true/false into 1/0 raw[key] = Boolean(value) } else { raw[key] = isOptional ? null : false } } else { // type = number // Treat NaN and Infinity as null if (isValidNumber(value)) { raw[key] = value || 0 } else { raw[key] = isOptional ? null : 0 } } } function isValidStatus(value: any): boolean { return value === 'created' || value === 'updated' || value === 'deleted' || value === 'synced' } // Transforms a dirty raw record object into a trusted sanitized RawRecord according to passed TableSchema // TODO: Should we make this public API for advanced users? export function sanitizedRaw(dirtyRaw: DirtyRaw, tableSchema: TableSchema): RawRecord { const { id, _status, _changed } = dirtyRaw // This is called with `{}` when making a new record, so we need to set a new ID, status // Also: If an existing has one of those fields broken, we're screwed. Safest to treat it as a // new record (so that it gets synced) // TODO: Think about whether prototypeless objects are a useful mitigation // const raw = Object.create(null) // create a prototypeless object const raw: $Shape<RawRecord> = {} if (typeof id === 'string') { // TODO: Can we trust IDs passed? Maybe we want to split this implementation, depending on whether // this is used on implicitly-trusted (persisted or Watermelon-created) records, or if this is user input? raw.id = id raw._status = isValidStatus(_status) ? _status : 'created' raw._changed = typeof _changed === 'string' ? _changed : '' } else { raw.id = randomId() raw._status = 'created' raw._changed = '' } // faster than Object.values on a map const columns = tableSchema.columnArray for (let i = 0, len = columns.length; i < len; i += 1) { const columnSchema = columns[i] const key = (columnSchema.name: string) // TODO: Check performance // $FlowFixMe const value = Object.prototype.hasOwnProperty.call(dirtyRaw, key) ? dirtyRaw[key] : null _setRaw(raw, key, value, columnSchema) } return (raw: any) } // Modifies passed rawRecord by setting sanitized `value` to `columnName` // Note: Assumes columnName exists and columnSchema matches the name export function setRawSanitized( rawRecord: RawRecord, columnName: ColumnName, value: any, columnSchema: ColumnSchema, ): void { _setRaw(rawRecord, columnName, value, columnSchema) } export type NullValue = null | '' | 0 | false export function nullValue(columnSchema: ColumnSchema): NullValue { const { isOptional, type } = columnSchema if (isOptional) { return null } else if (type === 'string') { return '' } else if (type === 'number') { return 0 } else if (type === 'boolean') { return false } throw new Error(`Unknown type for column schema ${JSON.stringify(columnSchema)}`) }