xref: /expo/packages/expo-sqlite/src/SQLite.ts (revision 79294b5e)
1import './polyfillNextTick';
2
3import customOpenDatabase from '@expo/websql/custom';
4import { requireNativeModule } from 'expo-modules-core';
5import { Platform } from 'react-native';
6
7import { Query, ResultSet, ResultSetError, SQLiteCallback, WebSQLDatabase } from './SQLite.types';
8
9const ExpoSQLite = requireNativeModule('ExpoSQLite');
10
11function zipObject(keys: string[], values: any[]) {
12  const result = {};
13  for (let i = 0; i < keys.length; i++) {
14    result[keys[i]] = values[i];
15  }
16  return result;
17}
18
19class SQLiteDatabase {
20  _name: string;
21  _closed: boolean = false;
22
23  constructor(name: string) {
24    this._name = name;
25  }
26
27  exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void {
28    if (this._closed) {
29      throw new Error(`The SQLite database is closed`);
30    }
31
32    ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then(
33      (nativeResultSets) => {
34        callback(null, nativeResultSets.map(_deserializeResultSet));
35      },
36      (error) => {
37        // TODO: make the native API consistently reject with an error, not a string or other type
38        callback(error instanceof Error ? error : new Error(error));
39      }
40    );
41  }
42
43  close() {
44    this._closed = true;
45    return ExpoSQLite.close(this._name);
46  }
47
48  deleteAsync(): Promise<void> {
49    if (!this._closed) {
50      throw new Error(
51        `Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.`
52      );
53    }
54
55    return ExpoSQLite.deleteAsync(this._name);
56  }
57}
58
59function _serializeQuery(query: Query): Query | [string, any[]] {
60  return Platform.OS === 'android'
61    ? {
62        sql: query.sql,
63        args: query.args.map(_escapeBlob),
64      }
65    : [query.sql, query.args];
66}
67
68function _deserializeResultSet(nativeResult): ResultSet | ResultSetError {
69  const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult;
70  // TODO: send more structured error information from the native module so we can better construct
71  // a SQLException object
72  if (errorMessage !== null) {
73    return { error: new Error(errorMessage) } as ResultSetError;
74  }
75
76  return {
77    insertId,
78    rowsAffected,
79    rows: rows.map((row) => zipObject(columns, row)),
80  };
81}
82
83function _escapeBlob<T>(data: T): T {
84  if (typeof data === 'string') {
85    /* eslint-disable no-control-regex */
86    return data
87      .replace(/\u0002/g, '\u0002\u0002')
88      .replace(/\u0001/g, '\u0001\u0002')
89      .replace(/\u0000/g, '\u0001\u0001') as any;
90    /* eslint-enable no-control-regex */
91  } else {
92    return data;
93  }
94}
95
96const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase);
97
98// @needsAudit @docsMissing
99/**
100 * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk,
101 * the database will be created under the app's [documents directory](./filesystem), i.e.
102 * `${FileSystem.documentDirectory}/SQLite/${name}`.
103 * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function
104 * for compatibility with the WebSQL specification.
105 * @param name Name of the database file to open.
106 * @param version
107 * @param description
108 * @param size
109 * @param callback
110 * @return
111 */
112export function openDatabase(
113  name: string,
114  version: string = '1.0',
115  description: string = name,
116  size: number = 1,
117  callback?: (db: WebSQLDatabase) => void
118): WebSQLDatabase {
119  if (name === undefined) {
120    throw new TypeError(`The database name must not be undefined`);
121  }
122  const db = _openExpoSQLiteDatabase(name, version, description, size, callback);
123  db.exec = db._db.exec.bind(db._db);
124  db.closeAsync = db._db.close.bind(db._db);
125  db.deleteAsync = db._db.deleteAsync.bind(db._db);
126  return db;
127}
128