1import './polyfillNextTick'; 2import customOpenDatabase from '@expo/websql/custom'; 3import { requireNativeModule, EventEmitter } from 'expo-modules-core'; 4import { Platform } from 'react-native'; 5const ExpoSQLite = requireNativeModule('ExpoSQLite'); 6const emitter = new EventEmitter(ExpoSQLite); 7function zipObject(keys, values) { 8 const result = {}; 9 for (let i = 0; i < keys.length; i++) { 10 result[keys[i]] = values[i]; 11 } 12 return result; 13} 14/** The database returned by `openDatabase()` */ 15export class SQLiteDatabase { 16 _name; 17 _closed = false; 18 constructor(name) { 19 this._name = name; 20 } 21 /** 22 * Executes the SQL statement and returns a callback resolving with the result. 23 */ 24 exec(queries, readOnly, callback) { 25 if (this._closed) { 26 throw new Error(`The SQLite database is closed`); 27 } 28 ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then((nativeResultSets) => { 29 callback(null, nativeResultSets.map(_deserializeResultSet)); 30 }, (error) => { 31 // TODO: make the native API consistently reject with an error, not a string or other type 32 callback(error instanceof Error ? error : new Error(error)); 33 }); 34 } 35 /** 36 * Due to limitations on `Android` this function is provided to allow raw SQL queries to be 37 * executed on the database. This will be less efficient than using the `exec` function, please use 38 * only when necessary. 39 */ 40 execRawQuery(queries, readOnly, callback) { 41 if (Platform.OS === 'ios') { 42 return this.exec(queries, readOnly, callback); 43 } 44 ExpoSQLite.execRawQuery(this._name, queries.map(_serializeQuery), readOnly).then((nativeResultSets) => { 45 callback(null, nativeResultSets.map(_deserializeResultSet)); 46 }, (error) => { 47 callback(error instanceof Error ? error : new Error(error)); 48 }); 49 } 50 /** 51 * Executes the SQL statement and returns a Promise resolving with the result. 52 */ 53 async execAsync(queries, readOnly) { 54 if (this._closed) { 55 throw new Error(`The SQLite database is closed`); 56 } 57 const nativeResultSets = await ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly); 58 return nativeResultSets.map(_deserializeResultSet); 59 } 60 /** 61 * @deprecated Use `closeAsync()` instead. 62 */ 63 close = this.closeAsync; 64 /** 65 * Close the database. 66 */ 67 closeAsync() { 68 this._closed = true; 69 return ExpoSQLite.close(this._name); 70 } 71 /** 72 * Synchronously closes the database. 73 */ 74 closeSync() { 75 this._closed = true; 76 return ExpoSQLite.closeSync(this._name); 77 } 78 /** 79 * Delete the database file. 80 * > The database has to be closed prior to deletion. 81 */ 82 deleteAsync() { 83 if (!this._closed) { 84 throw new Error(`Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.`); 85 } 86 return ExpoSQLite.deleteAsync(this._name); 87 } 88 /** 89 * Used to listen to changes in the database. 90 * @param callback A function that receives the `tableName` and `rowId` of the modified data. 91 */ 92 onDatabaseChange(cb) { 93 return emitter.addListener('onDatabaseChange', cb); 94 } 95 /** 96 * Creates a new transaction with Promise support. 97 * @param asyncCallback A `SQLTransactionAsyncCallback` function that can perform SQL statements in a transaction. 98 * @param readOnly true if all the SQL statements in the callback are read only. 99 */ 100 async transactionAsync(asyncCallback, readOnly = false) { 101 await this.execAsync([{ sql: 'BEGIN;', args: [] }], false); 102 try { 103 const transaction = new ExpoSQLTransactionAsync(this, readOnly); 104 await asyncCallback(transaction); 105 await this.execAsync([{ sql: 'END;', args: [] }], false); 106 } 107 catch (e) { 108 await this.execAsync([{ sql: 'ROLLBACK;', args: [] }], false); 109 throw e; 110 } 111 } 112 // @ts-expect-error: properties that are added from websql 113 version; 114} 115function _serializeQuery(query) { 116 return Platform.OS === 'android' 117 ? { 118 sql: query.sql, 119 args: query.args.map(_escapeBlob), 120 } 121 : [query.sql, query.args]; 122} 123function _deserializeResultSet(nativeResult) { 124 const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult; 125 // TODO: send more structured error information from the native module so we can better construct 126 // a SQLException object 127 if (errorMessage !== null) { 128 return { error: new Error(errorMessage) }; 129 } 130 return { 131 insertId, 132 rowsAffected, 133 rows: rows.map((row) => zipObject(columns, row)), 134 }; 135} 136function _escapeBlob(data) { 137 if (typeof data === 'string') { 138 /* eslint-disable no-control-regex */ 139 return data 140 .replace(/\u0002/g, '\u0002\u0002') 141 .replace(/\u0001/g, '\u0001\u0002') 142 .replace(/\u0000/g, '\u0001\u0001'); 143 /* eslint-enable no-control-regex */ 144 } 145 else { 146 return data; 147 } 148} 149const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase); 150// @needsAudit @docsMissing 151/** 152 * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk, 153 * the database will be created under the app's [documents directory](./filesystem), i.e. 154 * `${FileSystem.documentDirectory}/SQLite/${name}`. 155 * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function 156 * for compatibility with the WebSQL specification. 157 * @param name Name of the database file to open. 158 * @param version 159 * @param description 160 * @param size 161 * @param callback 162 * @return 163 */ 164export function openDatabase(name, version = '1.0', description = name, size = 1, callback) { 165 if (name === undefined) { 166 throw new TypeError(`The database name must not be undefined`); 167 } 168 const db = _openExpoSQLiteDatabase(name, version, description, size, callback); 169 db.exec = db._db.exec.bind(db._db); 170 db.execRawQuery = db._db.execRawQuery.bind(db._db); 171 db.execAsync = db._db.execAsync.bind(db._db); 172 db.closeAsync = db._db.closeAsync.bind(db._db); 173 db.closeSync = db._db.closeSync.bind(db._db); 174 db.onDatabaseChange = db._db.onDatabaseChange.bind(db._db); 175 db.deleteAsync = db._db.deleteAsync.bind(db._db); 176 db.transactionAsync = db._db.transactionAsync.bind(db._db); 177 return db; 178} 179/** 180 * Internal data structure for the async transaction API. 181 * @internal 182 */ 183export class ExpoSQLTransactionAsync { 184 db; 185 readOnly; 186 constructor(db, readOnly) { 187 this.db = db; 188 this.readOnly = readOnly; 189 } 190 async executeSqlAsync(sqlStatement, args) { 191 const resultSets = await this.db.execAsync([{ sql: sqlStatement, args: args ?? [] }], this.readOnly); 192 const result = resultSets[0]; 193 if (isResultSetError(result)) { 194 throw result.error; 195 } 196 return result; 197 } 198} 199function isResultSetError(result) { 200 return 'error' in result; 201} 202//# sourceMappingURL=SQLite.js.map