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