1import './polyfillNextTick'; 2 3import customOpenDatabase from '@expo/websql/custom'; 4import { NativeModulesProxy } from 'expo-modules-core'; 5import { Platform } from 'react-native'; 6 7import { Query, ResultSet, ResultSetError, SQLiteCallback, WebSQLDatabase } from './SQLite.types'; 8 9const { ExponentSQLite } = NativeModulesProxy; 10 11function zipObject(keys: string[], values: any[]) { 12 const result = {}; 13 for (let i = 0; i < keys.length; i++) { 14 result[keys[i]] = values[i]; 15 } 16 return result; 17} 18 19class SQLiteDatabase { 20 _name: string; 21 _closed: boolean = false; 22 23 constructor(name: string) { 24 this._name = name; 25 } 26 27 exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void { 28 if (this._closed) { 29 throw new Error(`The SQLite database is closed`); 30 } 31 32 ExponentSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then( 33 (nativeResultSets) => { 34 callback(null, nativeResultSets.map(_deserializeResultSet)); 35 }, 36 (error) => { 37 // TODO: make the native API consistently reject with an error, not a string or other type 38 callback(error instanceof Error ? error : new Error(error)); 39 } 40 ); 41 } 42 43 close() { 44 this._closed = true; 45 return ExponentSQLite.close(this._name); 46 } 47 48 deleteAsync(): Promise<void> { 49 if (!this._closed) { 50 throw new Error( 51 `Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.` 52 ); 53 } 54 55 return ExponentSQLite.deleteAsync(this._name); 56 } 57} 58 59function _serializeQuery(query: Query): [string, unknown[]] { 60 return [query.sql, Platform.OS === 'android' ? query.args.map(_escapeBlob) : query.args]; 61} 62 63function _deserializeResultSet(nativeResult): ResultSet | ResultSetError { 64 const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult; 65 // TODO: send more structured error information from the native module so we can better construct 66 // a SQLException object 67 if (errorMessage !== null) { 68 return { error: new Error(errorMessage) } as ResultSetError; 69 } 70 71 return { 72 insertId, 73 rowsAffected, 74 rows: rows.map((row) => zipObject(columns, row)), 75 }; 76} 77 78function _escapeBlob<T>(data: T): T { 79 if (typeof data === 'string') { 80 /* eslint-disable no-control-regex */ 81 return data 82 .replace(/\u0002/g, '\u0002\u0002') 83 .replace(/\u0001/g, '\u0001\u0002') 84 .replace(/\u0000/g, '\u0001\u0001') as any; 85 /* eslint-enable no-control-regex */ 86 } else { 87 return data; 88 } 89} 90 91const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase); 92 93// @needsAudit @docsMissing 94/** 95 * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk, 96 * the database will be created under the app's [documents directory](./filesystem), i.e. 97 * `${FileSystem.documentDirectory}/SQLite/${name}`. 98 * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function 99 * for compatibility with the WebSQL specification. 100 * @param name Name of the database file to open. 101 * @param version 102 * @param description 103 * @param size 104 * @param callback 105 * @return 106 */ 107export function openDatabase( 108 name: string, 109 version: string = '1.0', 110 description: string = name, 111 size: number = 1, 112 callback?: (db: WebSQLDatabase) => void 113): WebSQLDatabase { 114 if (name === undefined) { 115 throw new TypeError(`The database name must not be undefined`); 116 } 117 const db = _openExpoSQLiteDatabase(name, version, description, size, callback); 118 db.exec = db._db.exec.bind(db._db); 119 db.closeAsync = db._db.close.bind(db._db); 120 db.deleteAsync = db._db.deleteAsync.bind(db._db); 121 return db; 122} 123