12ee87287SSzymon20000import './polyfillNextTick'; 22ee87287SSzymon20000 392f6de58SWill Schurmanimport customOpenDatabase from '@expo/websql/custom'; 474e0b8dfSAlan Hughesimport { requireNativeModule, EventEmitter } from 'expo-modules-core'; 52ee87287SSzymon20000import { Platform } from 'react-native'; 692f6de58SWill Schurman 74a7bfa1dSKudo Chienimport type { 84a7bfa1dSKudo Chien Query, 94a7bfa1dSKudo Chien ResultSet, 104a7bfa1dSKudo Chien ResultSetError, 114a7bfa1dSKudo Chien SQLiteCallback, 124a7bfa1dSKudo Chien SQLTransactionAsyncCallback, 134a7bfa1dSKudo Chien SQLTransactionAsync, 14c52e83e3SKudo Chien SQLTransactionCallback, 15c52e83e3SKudo Chien SQLTransactionErrorCallback, 164a7bfa1dSKudo Chien} from './SQLite.types'; 172ee87287SSzymon20000 18c1d37355SAlan Hughesconst ExpoSQLite = requireNativeModule('ExpoSQLite'); 1974e0b8dfSAlan Hughesconst emitter = new EventEmitter(ExpoSQLite); 202ee87287SSzymon20000 21e2a59be1SEvan Baconfunction zipObject(keys: string[], values: any[]) { 22e2a59be1SEvan Bacon const result = {}; 23e2a59be1SEvan Bacon for (let i = 0; i < keys.length; i++) { 24e2a59be1SEvan Bacon result[keys[i]] = values[i]; 25e2a59be1SEvan Bacon } 26e2a59be1SEvan Bacon return result; 27e2a59be1SEvan Bacon} 28e2a59be1SEvan Bacon 294a7bfa1dSKudo Chien/** The database returned by `openDatabase()` */ 304a7bfa1dSKudo Chienexport class SQLiteDatabase { 312ee87287SSzymon20000 _name: string; 322ee87287SSzymon20000 _closed: boolean = false; 332ee87287SSzymon20000 342ee87287SSzymon20000 constructor(name: string) { 352ee87287SSzymon20000 this._name = name; 362ee87287SSzymon20000 } 372ee87287SSzymon20000 384a7bfa1dSKudo Chien /** 394a7bfa1dSKudo Chien * Executes the SQL statement and returns a callback resolving with the result. 404a7bfa1dSKudo Chien */ 415cb26b36STomo Krajina exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void { 422ee87287SSzymon20000 if (this._closed) { 432ee87287SSzymon20000 throw new Error(`The SQLite database is closed`); 442ee87287SSzymon20000 } 452ee87287SSzymon20000 46c1d37355SAlan Hughes ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then( 4735f78160SBartosz Kaszubowski (nativeResultSets) => { 482ee87287SSzymon20000 callback(null, nativeResultSets.map(_deserializeResultSet)); 492ee87287SSzymon20000 }, 5035f78160SBartosz Kaszubowski (error) => { 512ee87287SSzymon20000 // TODO: make the native API consistently reject with an error, not a string or other type 522ee87287SSzymon20000 callback(error instanceof Error ? error : new Error(error)); 532ee87287SSzymon20000 } 542ee87287SSzymon20000 ); 552ee87287SSzymon20000 } 562ee87287SSzymon20000 574a7bfa1dSKudo Chien /** 5850e16f8cSAlan Hughes * Due to limitations on `Android` this function is provided to allow raw SQL queries to be 5950e16f8cSAlan Hughes * executed on the database. This will be less efficient than using the `exec` function, please use 6050e16f8cSAlan Hughes * only when necessary. 6150e16f8cSAlan Hughes */ 6250e16f8cSAlan Hughes execRawQuery(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void { 6350e16f8cSAlan Hughes if (Platform.OS === 'ios') { 6450e16f8cSAlan Hughes return this.exec(queries, readOnly, callback); 6550e16f8cSAlan Hughes } 6650e16f8cSAlan Hughes 6750e16f8cSAlan Hughes ExpoSQLite.execRawQuery(this._name, queries.map(_serializeQuery), readOnly).then( 6850e16f8cSAlan Hughes (nativeResultSets) => { 6950e16f8cSAlan Hughes callback(null, nativeResultSets.map(_deserializeResultSet)); 7050e16f8cSAlan Hughes }, 7150e16f8cSAlan Hughes (error) => { 7250e16f8cSAlan Hughes callback(error instanceof Error ? error : new Error(error)); 7350e16f8cSAlan Hughes } 7450e16f8cSAlan Hughes ); 7550e16f8cSAlan Hughes } 7650e16f8cSAlan Hughes 7750e16f8cSAlan Hughes /** 784a7bfa1dSKudo Chien * Executes the SQL statement and returns a Promise resolving with the result. 794a7bfa1dSKudo Chien */ 80c52e83e3SKudo Chien async execAsync(queries: Query[], readOnly: boolean): Promise<(ResultSetError | ResultSet)[]> { 814a7bfa1dSKudo Chien if (this._closed) { 824a7bfa1dSKudo Chien throw new Error(`The SQLite database is closed`); 834a7bfa1dSKudo Chien } 844a7bfa1dSKudo Chien 854a7bfa1dSKudo Chien const nativeResultSets = await ExpoSQLite.exec( 864a7bfa1dSKudo Chien this._name, 874a7bfa1dSKudo Chien queries.map(_serializeQuery), 884a7bfa1dSKudo Chien readOnly 894a7bfa1dSKudo Chien ); 904a7bfa1dSKudo Chien return nativeResultSets.map(_deserializeResultSet); 914a7bfa1dSKudo Chien } 924a7bfa1dSKudo Chien 934a7bfa1dSKudo Chien /** 944a7bfa1dSKudo Chien * @deprecated Use `closeAsync()` instead. 954a7bfa1dSKudo Chien */ 964a7bfa1dSKudo Chien close = this.closeAsync; 974a7bfa1dSKudo Chien 984a7bfa1dSKudo Chien /** 994a7bfa1dSKudo Chien * Close the database. 1004a7bfa1dSKudo Chien */ 1016c4baee8SAlan Hughes closeAsync(): Promise<void> { 1022ee87287SSzymon20000 this._closed = true; 103c1d37355SAlan Hughes return ExpoSQLite.close(this._name); 1043f4e5fe5SKudo Chien } 1053f4e5fe5SKudo Chien 1064a7bfa1dSKudo Chien /** 1076c4baee8SAlan Hughes * Synchronously closes the database. 1086c4baee8SAlan Hughes */ 1096c4baee8SAlan Hughes closeSync(): void { 1106c4baee8SAlan Hughes this._closed = true; 1116c4baee8SAlan Hughes return ExpoSQLite.closeSync(this._name); 1126c4baee8SAlan Hughes } 1136c4baee8SAlan Hughes 1146c4baee8SAlan Hughes /** 1154a7bfa1dSKudo Chien * Delete the database file. 1164a7bfa1dSKudo Chien * > The database has to be closed prior to deletion. 1174a7bfa1dSKudo Chien */ 1183f4e5fe5SKudo Chien deleteAsync(): Promise<void> { 1193f4e5fe5SKudo Chien if (!this._closed) { 1203f4e5fe5SKudo Chien throw new Error( 1213f4e5fe5SKudo Chien `Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.` 1223f4e5fe5SKudo Chien ); 1233f4e5fe5SKudo Chien } 1243f4e5fe5SKudo Chien 125c1d37355SAlan Hughes return ExpoSQLite.deleteAsync(this._name); 1262ee87287SSzymon20000 } 1274a7bfa1dSKudo Chien 12850e16f8cSAlan Hughes /** 12950e16f8cSAlan Hughes * Used to listen to changes in the database. 13050e16f8cSAlan Hughes * @param callback A function that receives the `tableName` and `rowId` of the modified data. 13150e16f8cSAlan Hughes */ 13274e0b8dfSAlan Hughes onDatabaseChange(cb: (result: { tableName: string; rowId: number }) => void) { 13374e0b8dfSAlan Hughes return emitter.addListener('onDatabaseChange', cb); 13474e0b8dfSAlan Hughes } 13574e0b8dfSAlan Hughes 1364a7bfa1dSKudo Chien /** 1374a7bfa1dSKudo Chien * Creates a new transaction with Promise support. 1384a7bfa1dSKudo Chien * @param asyncCallback A `SQLTransactionAsyncCallback` function that can perform SQL statements in a transaction. 1394a7bfa1dSKudo Chien * @param readOnly true if all the SQL statements in the callback are read only. 1404a7bfa1dSKudo Chien */ 1414a7bfa1dSKudo Chien async transactionAsync( 1424a7bfa1dSKudo Chien asyncCallback: SQLTransactionAsyncCallback, 1434a7bfa1dSKudo Chien readOnly: boolean = false 1444a7bfa1dSKudo Chien ): Promise<void> { 1454a7bfa1dSKudo Chien await this.execAsync([{ sql: 'BEGIN;', args: [] }], false); 1464a7bfa1dSKudo Chien try { 1474a7bfa1dSKudo Chien const transaction = new ExpoSQLTransactionAsync(this, readOnly); 1484a7bfa1dSKudo Chien await asyncCallback(transaction); 1494a7bfa1dSKudo Chien await this.execAsync([{ sql: 'END;', args: [] }], false); 1504a7bfa1dSKudo Chien } catch (e: unknown) { 1514a7bfa1dSKudo Chien await this.execAsync([{ sql: 'ROLLBACK;', args: [] }], false); 1524a7bfa1dSKudo Chien throw e; 1534a7bfa1dSKudo Chien } 1544a7bfa1dSKudo Chien } 155c52e83e3SKudo Chien 156c52e83e3SKudo Chien // @ts-expect-error: properties that are added from websql 157c52e83e3SKudo Chien version: string; 158c52e83e3SKudo Chien 159c52e83e3SKudo Chien /** 160c52e83e3SKudo Chien * Execute a database transaction. 161c52e83e3SKudo Chien * @param callback A function representing the transaction to perform. Takes a Transaction 162c52e83e3SKudo Chien * (see below) as its only parameter, on which it can add SQL statements to execute. 163c52e83e3SKudo Chien * @param errorCallback Called if an error occurred processing this transaction. Takes a single 164c52e83e3SKudo Chien * parameter describing the error. 165c52e83e3SKudo Chien * @param successCallback Called when the transaction has completed executing on the database. 166c52e83e3SKudo Chien */ 167c52e83e3SKudo Chien // @ts-expect-error: properties that are added from websql 168c52e83e3SKudo Chien transaction( 169c52e83e3SKudo Chien callback: SQLTransactionCallback, 170c52e83e3SKudo Chien errorCallback?: SQLTransactionErrorCallback, 171c52e83e3SKudo Chien successCallback?: () => void 172c52e83e3SKudo Chien ): void; 173c52e83e3SKudo Chien 174c52e83e3SKudo Chien // @ts-expect-error: properties that are added from websql 175c52e83e3SKudo Chien readTransaction( 176c52e83e3SKudo Chien callback: SQLTransactionCallback, 177c52e83e3SKudo Chien errorCallback?: SQLTransactionErrorCallback, 178c52e83e3SKudo Chien successCallback?: () => void 179c52e83e3SKudo Chien ): void; 1802ee87287SSzymon20000} 1812ee87287SSzymon20000 18209ee9580SAlan Hughesfunction _serializeQuery(query: Query): Query | [string, any[]] { 18309ee9580SAlan Hughes return Platform.OS === 'android' 18409ee9580SAlan Hughes ? { 18509ee9580SAlan Hughes sql: query.sql, 18609ee9580SAlan Hughes args: query.args.map(_escapeBlob), 18709ee9580SAlan Hughes } 18809ee9580SAlan Hughes : [query.sql, query.args]; 1892ee87287SSzymon20000} 1902ee87287SSzymon20000 1915cb26b36STomo Krajinafunction _deserializeResultSet(nativeResult): ResultSet | ResultSetError { 192b91f886fSJames Ide const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult; 1932ee87287SSzymon20000 // TODO: send more structured error information from the native module so we can better construct 1942ee87287SSzymon20000 // a SQLException object 1952ee87287SSzymon20000 if (errorMessage !== null) { 1965cb26b36STomo Krajina return { error: new Error(errorMessage) } as ResultSetError; 1972ee87287SSzymon20000 } 1982ee87287SSzymon20000 1992ee87287SSzymon20000 return { 2002ee87287SSzymon20000 insertId, 2012ee87287SSzymon20000 rowsAffected, 20235f78160SBartosz Kaszubowski rows: rows.map((row) => zipObject(columns, row)), 2032ee87287SSzymon20000 }; 2042ee87287SSzymon20000} 2052ee87287SSzymon20000 2062ee87287SSzymon20000function _escapeBlob<T>(data: T): T { 2072ee87287SSzymon20000 if (typeof data === 'string') { 2082ee87287SSzymon20000 /* eslint-disable no-control-regex */ 2092ee87287SSzymon20000 return data 2102ee87287SSzymon20000 .replace(/\u0002/g, '\u0002\u0002') 2112ee87287SSzymon20000 .replace(/\u0001/g, '\u0001\u0002') 2122ee87287SSzymon20000 .replace(/\u0000/g, '\u0001\u0001') as any; 2132ee87287SSzymon20000 /* eslint-enable no-control-regex */ 2142ee87287SSzymon20000 } else { 2152ee87287SSzymon20000 return data; 2162ee87287SSzymon20000 } 2172ee87287SSzymon20000} 2182ee87287SSzymon20000 2192ee87287SSzymon20000const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase); 2202ee87287SSzymon20000 221abded2bbSBartosz Kaszubowski// @needsAudit @docsMissing 222abded2bbSBartosz Kaszubowski/** 223abded2bbSBartosz Kaszubowski * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk, 224a7112dadSBartosz Kaszubowski * the database will be created under the app's [documents directory](./filesystem), i.e. 225abded2bbSBartosz Kaszubowski * `${FileSystem.documentDirectory}/SQLite/${name}`. 226abded2bbSBartosz Kaszubowski * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function 227abded2bbSBartosz Kaszubowski * for compatibility with the WebSQL specification. 228abded2bbSBartosz Kaszubowski * @param name Name of the database file to open. 229abded2bbSBartosz Kaszubowski * @param version 230abded2bbSBartosz Kaszubowski * @param description 231abded2bbSBartosz Kaszubowski * @param size 232abded2bbSBartosz Kaszubowski * @param callback 233abded2bbSBartosz Kaszubowski * @return 234abded2bbSBartosz Kaszubowski */ 2352ee87287SSzymon20000export function openDatabase( 2362ee87287SSzymon20000 name: string, 2372ee87287SSzymon20000 version: string = '1.0', 2382ee87287SSzymon20000 description: string = name, 2392ee87287SSzymon20000 size: number = 1, 2404a7bfa1dSKudo Chien callback?: (db: SQLiteDatabase) => void 2414a7bfa1dSKudo Chien): SQLiteDatabase { 2422ee87287SSzymon20000 if (name === undefined) { 2432ee87287SSzymon20000 throw new TypeError(`The database name must not be undefined`); 2442ee87287SSzymon20000 } 2450002abeaSSzymon20000 const db = _openExpoSQLiteDatabase(name, version, description, size, callback); 2463f4e5fe5SKudo Chien db.exec = db._db.exec.bind(db._db); 24750e16f8cSAlan Hughes db.execRawQuery = db._db.execRawQuery.bind(db._db); 2484a7bfa1dSKudo Chien db.execAsync = db._db.execAsync.bind(db._db); 2494a7bfa1dSKudo Chien db.closeAsync = db._db.closeAsync.bind(db._db); 2506c4baee8SAlan Hughes db.closeSync = db._db.closeSync.bind(db._db); 25174e0b8dfSAlan Hughes db.onDatabaseChange = db._db.onDatabaseChange.bind(db._db); 2523f4e5fe5SKudo Chien db.deleteAsync = db._db.deleteAsync.bind(db._db); 2534a7bfa1dSKudo Chien db.transactionAsync = db._db.transactionAsync.bind(db._db); 2543f4e5fe5SKudo Chien return db; 2552ee87287SSzymon20000} 2564a7bfa1dSKudo Chien 2574a7bfa1dSKudo Chien/** 2584a7bfa1dSKudo Chien * Internal data structure for the async transaction API. 2594a7bfa1dSKudo Chien * @internal 2604a7bfa1dSKudo Chien */ 2614a7bfa1dSKudo Chienexport class ExpoSQLTransactionAsync implements SQLTransactionAsync { 2628a424bebSJames Ide constructor( 2638a424bebSJames Ide private readonly db: SQLiteDatabase, 2648a424bebSJames Ide private readonly readOnly: boolean 2658a424bebSJames Ide ) {} 2664a7bfa1dSKudo Chien 267*c4573fffSKudo Chien async executeSqlAsync(sqlStatement: string, args?: (number | string)[]): Promise<ResultSet> { 2684a7bfa1dSKudo Chien const resultSets = await this.db.execAsync( 2694a7bfa1dSKudo Chien [{ sql: sqlStatement, args: args ?? [] }], 2704a7bfa1dSKudo Chien this.readOnly 2714a7bfa1dSKudo Chien ); 272*c4573fffSKudo Chien const result = resultSets[0]; 273*c4573fffSKudo Chien if (isResultSetError(result)) { 274*c4573fffSKudo Chien throw result.error; 2754a7bfa1dSKudo Chien } 276*c4573fffSKudo Chien return result; 277*c4573fffSKudo Chien } 278*c4573fffSKudo Chien} 279*c4573fffSKudo Chien 280*c4573fffSKudo Chienfunction isResultSetError(result: ResultSet | ResultSetError): result is ResultSetError { 281*c4573fffSKudo Chien return 'error' in result; 2824a7bfa1dSKudo Chien} 283