UNPKG

expo-sqlite

Version:

Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.

359 lines 12.6 kB
import AwaitLock from 'await-lock'; import { openDatabaseAsync, openDatabaseSync } from './index'; const DATABASE_VERSION = 1; const STATEMENT_GET = 'SELECT value FROM storage WHERE key = ?;'; const STATEMENT_SET = 'INSERT INTO storage (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;'; const STATEMENT_REMOVE = 'DELETE FROM storage WHERE key = ?;'; const STATEMENT_GET_ALL_KEYS = 'SELECT key FROM storage;'; const STATEMENT_CLEAR = 'DELETE FROM storage;'; const MIGRATION_STATEMENT_0 = 'CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY NOT NULL, value TEXT);'; /** * Key-value store backed by SQLite. This class accepts a `databaseName` parameter in its constructor, which is the name of the database file to use for the storage. */ export class SQLiteStorage { databaseName; db = null; awaitLock = new AwaitLock(); constructor(databaseName) { this.databaseName = databaseName; } //#region Asynchronous API /** * Retrieves the value associated with the given key asynchronously. */ async getItemAsync(key) { this.checkValidInput(key); const db = await this.getDbAsync(); const result = await db.getFirstAsync(STATEMENT_GET, key); return result?.value ?? null; } /** * Sets the value for the given key asynchronously. * If a function is provided, it computes the new value based on the previous value. */ async setItemAsync(key, value) { this.checkValidInput(key, value); const db = await this.getDbAsync(); if (typeof value === 'function') { await db.withExclusiveTransactionAsync(async (tx) => { const prevResult = await tx.getFirstAsync(STATEMENT_GET, key); const prevValue = prevResult?.value ?? null; const nextValue = value(prevValue); this.checkValidInput(key, nextValue); await tx.runAsync(STATEMENT_SET, key, nextValue); }); return; } await db.runAsync(STATEMENT_SET, key, value); } /** * Removes the value associated with the given key asynchronously. */ async removeItemAsync(key) { this.checkValidInput(key); const db = await this.getDbAsync(); const result = await db.runAsync(STATEMENT_REMOVE, key); return result.changes > 0; } /** * Retrieves all keys stored in the storage asynchronously. */ async getAllKeysAsync() { const db = await this.getDbAsync(); const result = await db.getAllAsync(STATEMENT_GET_ALL_KEYS); return result.map(({ key }) => key); } /** * Clears all key-value pairs from the storage asynchronously. */ async clearAsync() { const db = await this.getDbAsync(); const result = await db.runAsync(STATEMENT_CLEAR); return result.changes > 0; } /** * Closes the database connection asynchronously. */ async closeAsync() { await this.awaitLock.acquireAsync(); try { if (this.db) { await this.db.closeAsync(); this.db = null; } } finally { this.awaitLock.release(); } } //#endregion //#region Synchronous API /** * Retrieves the value associated with the given key synchronously. */ getItemSync(key) { this.checkValidInput(key); const db = this.getDbSync(); const result = db.getFirstSync(STATEMENT_GET, key); return result?.value ?? null; } /** * Sets the value for the given key synchronously. * If a function is provided, it computes the new value based on the previous value. */ setItemSync(key, value) { this.checkValidInput(key, value); const db = this.getDbSync(); if (typeof value === 'function') { db.withTransactionSync(() => { const prevResult = db.getFirstSync(STATEMENT_GET, key); const prevValue = prevResult?.value ?? null; const nextValue = value(prevValue); this.checkValidInput(key, nextValue); db.runSync(STATEMENT_SET, key, nextValue); }); return; } db.runSync(STATEMENT_SET, key, value); } /** * Removes the value associated with the given key synchronously. */ removeItemSync(key) { this.checkValidInput(key); const db = this.getDbSync(); const result = db.runSync(STATEMENT_REMOVE, key); return result.changes > 0; } /** * Retrieves all keys stored in the storage synchronously. */ getAllKeysSync() { const db = this.getDbSync(); const result = db.getAllSync(STATEMENT_GET_ALL_KEYS); return result.map(({ key }) => key); } /** * Clears all key-value pairs from the storage synchronously. */ clearSync() { const db = this.getDbSync(); const result = db.runSync(STATEMENT_CLEAR); return result.changes > 0; } /** * Closes the database connection synchronously. */ closeSync() { if (this.db) { this.db.closeSync(); this.db = null; } } //#endregion //#region react-native-async-storage compatible API /** * Alias for [`getItemAsync()`](#getitemasynckey) method. */ async getItem(key) { return this.getItemAsync(key); } /** * Alias for [`setItemAsync()`](#setitemasynckey-value). */ async setItem(key, value) { await this.setItemAsync(key, value); } /** * Alias for [`removeItemAsync()`](#removeitemasynckey) method. */ async removeItem(key) { await this.removeItemAsync(key); } /** * Alias for [`getAllKeysAsync()`](#getallkeysasync) method. */ async getAllKeys() { return this.getAllKeysAsync(); } /** * Alias for [`clearAsync()`](#clearasync) method. */ async clear() { await this.clearAsync(); } /** * Merges the given value with the existing value for the given key asynchronously. * If the existing value is a JSON object, performs a deep merge. */ async mergeItem(key, value) { this.checkValidInput(key, value); await this.setItemAsync(key, (prevValue) => { if (prevValue == null) { return value; } const prevJSON = JSON.parse(prevValue); const newJSON = JSON.parse(value); const mergedJSON = SQLiteStorage.mergeDeep(prevJSON, newJSON); return JSON.stringify(mergedJSON); }); } /** * Retrieves the values associated with the given keys asynchronously. */ async multiGet(keys) { return Promise.all(keys.map(async (key) => { this.checkValidInput(key); return [key, await this.getItemAsync(key)]; })); } /** * Sets multiple key-value pairs asynchronously. */ async multiSet(keyValuePairs) { const db = await this.getDbAsync(); await db.withExclusiveTransactionAsync(async (tx) => { for (const [key, value] of keyValuePairs) { this.checkValidInput(key, value); await tx.runAsync(STATEMENT_SET, key, value); } }); } /** * Removes the values associated with the given keys asynchronously. */ async multiRemove(keys) { const db = await this.getDbAsync(); await db.withExclusiveTransactionAsync(async (tx) => { for (const key of keys) { this.checkValidInput(key); await tx.runAsync(STATEMENT_REMOVE, key); } }); } /** * Merges multiple key-value pairs asynchronously. * If existing values are JSON objects, performs a deep merge. */ async multiMerge(keyValuePairs) { const db = await this.getDbAsync(); await db.withExclusiveTransactionAsync(async (tx) => { for (const [key, value] of keyValuePairs) { this.checkValidInput(key, value); const prevValue = await tx.getFirstAsync(STATEMENT_GET, key); if (prevValue == null) { await tx.runAsync(STATEMENT_SET, key, value); continue; } const prevJSON = JSON.parse(prevValue.value); const newJSON = JSON.parse(value); const mergedJSON = SQLiteStorage.mergeDeep(prevJSON, newJSON); await tx.runAsync(STATEMENT_SET, key, JSON.stringify(mergedJSON)); } }); } /** * Alias for [`closeAsync()`](#closeasync-1) method. */ async close() { await this.closeAsync(); } //#endregion //#region Internals async getDbAsync() { await this.awaitLock.acquireAsync(); try { if (!this.db) { const db = await openDatabaseAsync(this.databaseName); await this.maybeMigrateDbAsync(db); this.db = db; } } finally { this.awaitLock.release(); } return this.db; } getDbSync() { if (!this.db) { const db = openDatabaseSync(this.databaseName); this.maybeMigrateDbSync(db); this.db = db; } return this.db; } maybeMigrateDbAsync(db) { return db.withTransactionAsync(async () => { const result = await db.getFirstAsync('PRAGMA user_version'); let currentDbVersion = result?.user_version ?? 0; if (currentDbVersion >= DATABASE_VERSION) { return; } if (currentDbVersion === 0) { await db.execAsync(MIGRATION_STATEMENT_0); currentDbVersion = 1; } await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); }); } maybeMigrateDbSync(db) { db.withTransactionSync(() => { const result = db.getFirstSync('PRAGMA user_version'); let currentDbVersion = result?.user_version ?? 0; if (currentDbVersion >= DATABASE_VERSION) { return; } if (currentDbVersion === 0) { db.execSync(MIGRATION_STATEMENT_0); currentDbVersion = 1; } db.execSync(`PRAGMA user_version = ${DATABASE_VERSION}`); }); } /** * Recursively merge two JSON objects. */ static mergeDeep(target, source) { if (typeof target !== 'object' || target === null) { return source; } if (typeof source !== 'object' || source === null) { return target; } const output = { ...target }; for (const key of Object.keys(source)) { if (source[key] instanceof Array) { if (!output[key]) { output[key] = []; } output[key] = output[key].concat(source[key]); } else if (typeof source[key] === 'object') { output[key] = this.mergeDeep(target[key], source[key]); } else { output[key] = source[key]; } } return output; } checkValidInput(...input) { const [key, value] = input; if (typeof key !== 'string') { throw new Error(`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${key}`); } if (input.length > 1 && typeof value !== 'string' && typeof value !== 'function') { throw new Error(`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${value}`); } } } /** * This default instance of the [`SQLiteStorage`](#sqlitestorage-1) class is used as a drop-in replacement for the `AsyncStorage` module from [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage). */ export const AsyncStorage = new SQLiteStorage('ExpoSQLiteStorage'); export default AsyncStorage; /** * Alias for [`AsyncStorage`](#sqliteasyncstorage), given the storage not only offers asynchronous methods. */ export const Storage = AsyncStorage; //# sourceMappingURL=Storage.js.map