1import './polyfillNextTick'; 2 3import customOpenDatabase from '@expo/websql/custom'; 4import { requireNativeModule } from 'expo-modules-core'; 5import { Platform } from 'react-native'; 6 7import { Query, ResultSet, ResultSetError, SQLiteCallback, WebSQLDatabase } from './SQLite.types'; 8 9const ExpoSQLite = requireNativeModule('ExpoSQLite'); 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 ExpoSQLite.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 ExpoSQLite.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 ExpoSQLite.deleteAsync(this._name); 56 } 57} 58 59function _serializeQuery(query: Query): Query | [string, any[]] { 60 return Platform.OS === 'android' 61 ? { 62 sql: query.sql, 63 args: query.args.map(_escapeBlob), 64 } 65 : [query.sql, query.args]; 66} 67 68function _deserializeResultSet(nativeResult): ResultSet | ResultSetError { 69 const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult; 70 // TODO: send more structured error information from the native module so we can better construct 71 // a SQLException object 72 if (errorMessage !== null) { 73 return { error: new Error(errorMessage) } as ResultSetError; 74 } 75 76 return { 77 insertId, 78 rowsAffected, 79 rows: rows.map((row) => zipObject(columns, row)), 80 }; 81} 82 83function _escapeBlob<T>(data: T): T { 84 if (typeof data === 'string') { 85 /* eslint-disable no-control-regex */ 86 return data 87 .replace(/\u0002/g, '\u0002\u0002') 88 .replace(/\u0001/g, '\u0001\u0002') 89 .replace(/\u0000/g, '\u0001\u0001') as any; 90 /* eslint-enable no-control-regex */ 91 } else { 92 return data; 93 } 94} 95 96const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase); 97 98// @needsAudit @docsMissing 99/** 100 * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk, 101 * the database will be created under the app's [documents directory](./filesystem), i.e. 102 * `${FileSystem.documentDirectory}/SQLite/${name}`. 103 * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function 104 * for compatibility with the WebSQL specification. 105 * @param name Name of the database file to open. 106 * @param version 107 * @param description 108 * @param size 109 * @param callback 110 * @return 111 */ 112export function openDatabase( 113 name: string, 114 version: string = '1.0', 115 description: string = name, 116 size: number = 1, 117 callback?: (db: WebSQLDatabase) => void 118): WebSQLDatabase { 119 if (name === undefined) { 120 throw new TypeError(`The database name must not be undefined`); 121 } 122 const db = _openExpoSQLiteDatabase(name, version, description, size, callback); 123 db.exec = db._db.exec.bind(db._db); 124 db.closeAsync = db._db.close.bind(db._db); 125 db.deleteAsync = db._db.deleteAsync.bind(db._db); 126 return db; 127} 128