1import './polyfillNextTick'; 2 3import zipObject from 'lodash/zipObject'; 4import { Platform } from 'react-native'; 5import { NativeModulesProxy } from '@unimodules/core'; 6import customOpenDatabase from '@expo/websql/custom'; 7 8const { ExponentSQLite } = NativeModulesProxy; 9 10export type Query = { sql: string; args: unknown[] }; 11 12export interface ResultSetError { 13 error: Error; 14}; 15export interface ResultSet { 16 insertId?: number; 17 rowsAffected: number; 18 rows: Array<{ [column: string]: any }>; 19}; 20 21export type SQLiteCallback = (error?: Error | null, resultSet?: Array<ResultSetError | ResultSet>) => void; 22 23class SQLiteDatabase { 24 _name: string; 25 _closed: boolean = false; 26 27 constructor(name: string) { 28 this._name = name; 29 } 30 31 exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void { 32 if (this._closed) { 33 throw new Error(`The SQLite database is closed`); 34 } 35 36 ExponentSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then( 37 nativeResultSets => { 38 callback(null, nativeResultSets.map(_deserializeResultSet)); 39 }, 40 error => { 41 // TODO: make the native API consistently reject with an error, not a string or other type 42 callback(error instanceof Error ? error : new Error(error)); 43 } 44 ); 45 } 46 47 close() { 48 this._closed = true; 49 ExponentSQLite.close(this._name); 50 } 51} 52 53function _serializeQuery(query: Query): [string, unknown[]] { 54 return [query.sql, Platform.OS === 'android' ? query.args.map(_escapeBlob) : query.args]; 55} 56 57function _deserializeResultSet(nativeResult): ResultSet | ResultSetError { 58 let [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult; 59 // TODO: send more structured error information from the native module so we can better construct 60 // a SQLException object 61 if (errorMessage !== null) { 62 return { error: new Error(errorMessage) } as ResultSetError; 63 } 64 65 return { 66 insertId, 67 rowsAffected, 68 rows: rows.map(row => zipObject(columns, row)), 69 }; 70} 71 72function _escapeBlob<T>(data: T): T { 73 if (typeof data === 'string') { 74 /* eslint-disable no-control-regex */ 75 return data 76 .replace(/\u0002/g, '\u0002\u0002') 77 .replace(/\u0001/g, '\u0001\u0002') 78 .replace(/\u0000/g, '\u0001\u0001') as any; 79 /* eslint-enable no-control-regex */ 80 } else { 81 return data; 82 } 83} 84 85const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase); 86 87function addExecMethod(db: any): WebSQLDatabase { 88 db.exec = (queries: Query[], readOnly: boolean, callback: SQLiteCallback): void => { 89 db._db.exec(queries, readOnly, callback); 90 } 91 return db; 92} 93 94export function openDatabase( 95 name: string, 96 version: string = '1.0', 97 description: string = name, 98 size: number = 1, 99 callback?: (db: WebSQLDatabase) => void 100): WebSQLDatabase { 101 if (name === undefined) { 102 throw new TypeError(`The database name must not be undefined`); 103 } 104 const db = _openExpoSQLiteDatabase(name, version, description, size, callback); 105 const dbWithExec = addExecMethod(db); 106 return dbWithExec; 107} 108 109export interface WebSQLDatabase { 110 exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void; 111} 112 113export default { 114 openDatabase, 115}; 116