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