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