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 * Executes the SQL statement and returns a Promise resolving with the result. 37 */ 38 async execAsync(queries, readOnly) { 39 if (this._closed) { 40 throw new Error(`The SQLite database is closed`); 41 } 42 const nativeResultSets = await ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly); 43 return nativeResultSets.map(_deserializeResultSet); 44 } 45 /** 46 * @deprecated Use `closeAsync()` instead. 47 */ 48 close = this.closeAsync; 49 /** 50 * Close the database. 51 */ 52 closeAsync() { 53 this._closed = true; 54 return ExpoSQLite.close(this._name); 55 } 56 /** 57 * Synchronously closes the database. 58 */ 59 closeSync() { 60 this._closed = true; 61 return ExpoSQLite.closeSync(this._name); 62 } 63 /** 64 * Delete the database file. 65 * > The database has to be closed prior to deletion. 66 */ 67 deleteAsync() { 68 if (!this._closed) { 69 throw new Error(`Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.`); 70 } 71 return ExpoSQLite.deleteAsync(this._name); 72 } 73 onDatabaseChange(cb) { 74 return emitter.addListener('onDatabaseChange', cb); 75 } 76 /** 77 * Creates a new transaction with Promise support. 78 * @param asyncCallback A `SQLTransactionAsyncCallback` function that can perform SQL statements in a transaction. 79 * @param readOnly true if all the SQL statements in the callback are read only. 80 */ 81 async transactionAsync(asyncCallback, readOnly = false) { 82 await this.execAsync([{ sql: 'BEGIN;', args: [] }], false); 83 try { 84 const transaction = new ExpoSQLTransactionAsync(this, readOnly); 85 await asyncCallback(transaction); 86 await this.execAsync([{ sql: 'END;', args: [] }], false); 87 } 88 catch (e) { 89 await this.execAsync([{ sql: 'ROLLBACK;', args: [] }], false); 90 throw e; 91 } 92 } 93 // @ts-expect-error: properties that are added from websql 94 version; 95} 96function _serializeQuery(query) { 97 return Platform.OS === 'android' 98 ? { 99 sql: query.sql, 100 args: query.args.map(_escapeBlob), 101 } 102 : [query.sql, query.args]; 103} 104function _deserializeResultSet(nativeResult) { 105 const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult; 106 // TODO: send more structured error information from the native module so we can better construct 107 // a SQLException object 108 if (errorMessage !== null) { 109 return { error: new Error(errorMessage) }; 110 } 111 return { 112 insertId, 113 rowsAffected, 114 rows: rows.map((row) => zipObject(columns, row)), 115 }; 116} 117function _escapeBlob(data) { 118 if (typeof data === 'string') { 119 /* eslint-disable no-control-regex */ 120 return data 121 .replace(/\u0002/g, '\u0002\u0002') 122 .replace(/\u0001/g, '\u0001\u0002') 123 .replace(/\u0000/g, '\u0001\u0001'); 124 /* eslint-enable no-control-regex */ 125 } 126 else { 127 return data; 128 } 129} 130const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase); 131// @needsAudit @docsMissing 132/** 133 * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk, 134 * the database will be created under the app's [documents directory](./filesystem), i.e. 135 * `${FileSystem.documentDirectory}/SQLite/${name}`. 136 * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function 137 * for compatibility with the WebSQL specification. 138 * @param name Name of the database file to open. 139 * @param version 140 * @param description 141 * @param size 142 * @param callback 143 * @return 144 */ 145export function openDatabase(name, version = '1.0', description = name, size = 1, callback) { 146 if (name === undefined) { 147 throw new TypeError(`The database name must not be undefined`); 148 } 149 const db = _openExpoSQLiteDatabase(name, version, description, size, callback); 150 db.exec = db._db.exec.bind(db._db); 151 db.execAsync = db._db.execAsync.bind(db._db); 152 db.closeAsync = db._db.closeAsync.bind(db._db); 153 db.closeSync = db._db.closeSync.bind(db._db); 154 db.onDatabaseChange = db._db.onDatabaseChange.bind(db._db); 155 db.deleteAsync = db._db.deleteAsync.bind(db._db); 156 db.transactionAsync = db._db.transactionAsync.bind(db._db); 157 return db; 158} 159/** 160 * Internal data structure for the async transaction API. 161 * @internal 162 */ 163export class ExpoSQLTransactionAsync { 164 db; 165 readOnly; 166 constructor(db, readOnly) { 167 this.db = db; 168 this.readOnly = readOnly; 169 } 170 async executeSqlAsync(sqlStatement, args) { 171 const resultSets = await this.db.execAsync([{ sql: sqlStatement, args: args ?? [] }], this.readOnly); 172 return resultSets[0]; 173 } 174} 175//# sourceMappingURL=SQLite.js.map