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