stream-chat-react-native-core
Version:
The official React Native and Expo components for Stream Chat, a service for building chat applications
228 lines (202 loc) • 7 kB
text/typescript
import type { _InternalDB, OPSQLiteProxy } from '@op-engineering/op-sqlite';
let sqlite: OPSQLiteProxy;
try {
sqlite = require('@op-engineering/op-sqlite');
} catch (e) {
// We want to throw the original error when remote debugger (e.g. Chrome) is enabled.
// SQLite can only be used when synchronous method invocations (JSI) are possible.
// e.g on-device debugger (e.g. Flipper).
const isRemoteDebuggerError = e instanceof Error && e.message.includes('Failed to install');
if (isRemoteDebuggerError) {
throw e;
}
// Reaching here will mean that QuickSQLite is not installed for one of the reasons
// 1. Running on regular expo, where we don't support offline storage yet.
// 2. Offline support is disabled, in which case this library is not installed.
}
import { Logger } from 'stream-chat';
import { DB_LOCATION, DB_NAME } from './constants';
import { tables } from './schema';
import { createCreateTableQuery } from './sqlite-utils/createCreateTableQuery';
import type { PreparedBatchQueries, PreparedQueries, Scalar, Table } from './types';
/**
* SqliteClient takes care of any direct interaction with sqlite.
* This way usage @op-engineering/op-sqlite package is scoped to a single class/file.
*/
export class SqliteClient {
static dbVersion = 16;
static dbName = DB_NAME;
static dbLocation = DB_LOCATION;
static logger: Logger | undefined;
static db: _InternalDB | undefined;
static getDbVersion = () => SqliteClient.dbVersion;
// Force a specific db version. This is mainly useful for testsuit.
static setDbVersion = (version: number) => (SqliteClient.dbVersion = version);
static openDB = async () => {
try {
if (sqlite === undefined) {
throw new Error(
'Please install "@op-engineering/op-sqlite" package to enable offline support',
);
}
this.db = sqlite.open({
location: SqliteClient.dbLocation,
name: SqliteClient.dbName,
});
await this.db?.execute('PRAGMA foreign_keys = ON', []);
} catch (e) {
this.logger?.('error', `Error opening database ${SqliteClient.dbName}`, {
error: e,
});
console.error(`Error opening database ${SqliteClient.dbName}: ${e}`);
}
};
static closeDB = () => {
try {
if (!this.db) {
throw new Error('DB is not open or initialized.');
}
this.db.close();
this.db = undefined;
} catch (e) {
this.logger?.('error', `Error closing database ${SqliteClient.dbName}`, {
error: e,
});
console.error(`Error closing database ${SqliteClient.dbName}: ${e}`);
}
};
static executeSqlBatch = async (queries: PreparedBatchQueries[]) => {
if (!queries || !queries.length) {
return;
}
try {
if (!this.db) {
throw new Error('DB is not open or initialized.');
}
// This is a workaround to make the executeBatch method work.
// It expects an empty array as the second argument in the individual queries if nothing present.
// Discussion - https://discord.com/channels/1301463257722126357/1324262993780932688/1330846910596251711
const finalQueries = queries.map((query) => {
if (query.length === 1) {
// @ts-ignore
query.push([]);
}
return query;
});
await this.db.executeBatch(finalQueries);
} catch (e) {
this.logger?.('error', 'SqlBatch queries failed', {
error: e,
queries,
});
throw new Error(`Queries failed: ${e}`);
}
};
static executeSql = async (query: string, params?: Scalar[]) => {
try {
if (!this.db) {
throw new Error('DB is not open or initialized.');
}
const { rows } = await this.db.execute(query, params);
return rows ? (rows as Record<string, string>[]) : [];
} catch (e) {
this.logger?.('error', 'Sql single query failed', {
error: e,
query,
});
throw new Error(`Query failed: ${e}: `);
}
};
static dropTables = async () => {
const queries: PreparedQueries[] = Object.keys(tables).map((table) => [
`DROP TABLE IF EXISTS ${table}`,
[],
]);
this.logger?.('info', 'Dropping tables', {
tables: Object.keys(tables),
});
await SqliteClient.executeSqlBatch(queries);
};
static deleteDatabase = () => {
this.logger?.('info', 'deleteDatabase', {
dbLocation: SqliteClient.dbLocation,
dbname: SqliteClient.dbName,
});
try {
if (!this.db) {
throw new Error('DB is not open or initialized.');
}
this.db.delete();
} catch (e) {
this.logger?.('error', 'Error deleting DB', {
dbLocation: SqliteClient.dbLocation,
dbname: SqliteClient.dbName,
error: e,
});
throw new Error(`Error deleting DB: ${e}`);
}
return true;
};
static initializeDatabase = async () => {
try {
await SqliteClient.openDB();
const version = await SqliteClient.getUserPragmaVersion();
if (version !== SqliteClient.dbVersion) {
SqliteClient.logger?.('info', 'DB version mismatch');
await SqliteClient.dropTables();
await SqliteClient.updateUserPragmaVersion(SqliteClient.dbVersion);
}
SqliteClient.logger?.('info', 'create tables if not exists', {
tables: Object.keys(tables),
});
const q = (Object.keys(tables) as Table[]).reduce<PreparedQueries[]>(
(queriesSoFar, tableName) => {
queriesSoFar.push(...createCreateTableQuery(tableName));
return queriesSoFar;
},
[],
);
await SqliteClient.executeSqlBatch(q);
return true;
} catch (e) {
console.log('Error initializing DB', e);
this.logger?.('error', 'Error initializing DB', {
dbLocation: SqliteClient.dbLocation,
dbname: SqliteClient.dbName,
error: e,
});
return false;
}
};
static updateUserPragmaVersion = async (version: number) => {
SqliteClient.logger?.('info', `updateUserPragmaVersion to ${version}`);
if (!this.db) {
throw new Error('DB is not open or initialized.');
}
await this.db.execute(`PRAGMA user_version = ${version}`, []);
};
static getUserPragmaVersion = async () => {
try {
if (!this.db) {
throw new Error('DB is not open or initialized.');
}
const { rows } = await this.db.execute('PRAGMA user_version', []);
const result = rows ? rows : [];
this.logger?.('info', 'getUserPragmaVersion', {
result,
});
return result[0].user_version as number;
} catch (e) {
console.log('Error getting user_version', e);
throw new Error(`Querying for user_version failed: ${e}`);
}
};
static resetDB = async () => {
this.logger?.('info', 'resetDB');
if (this.db) {
await SqliteClient.dropTables();
SqliteClient.closeDB();
}
await SqliteClient.initializeDatabase();
};
}