xref: /expo/packages/expo-sqlite/build/SQLite.js (revision 4bf00a55)
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