xref: /expo/packages/expo-sqlite/src/SQLite.ts (revision d8dcfb65)
1import './polyfillNextTick';
2
3import zipObject from 'lodash/zipObject';
4import { Platform } from 'react-native';
5import { NativeModulesProxy } from '@unimodules/core';
6import customOpenDatabase from '@expo/websql/custom';
7import { Query, SQLiteCallback, ResultSet, ResultSetError, WebSQLDatabase } from './SQLite.types';
8
9const { ExponentSQLite } = NativeModulesProxy;
10
11class SQLiteDatabase {
12  _name: string;
13  _closed: boolean = false;
14
15  constructor(name: string) {
16    this._name = name;
17  }
18
19  exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void {
20    if (this._closed) {
21      throw new Error(`The SQLite database is closed`);
22    }
23
24    ExponentSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then(
25      nativeResultSets => {
26        callback(null, nativeResultSets.map(_deserializeResultSet));
27      },
28      error => {
29        // TODO: make the native API consistently reject with an error, not a string or other type
30        callback(error instanceof Error ? error : new Error(error));
31      }
32    );
33  }
34
35  close() {
36    this._closed = true;
37    ExponentSQLite.close(this._name);
38  }
39}
40
41function _serializeQuery(query: Query): [string, unknown[]] {
42  return [query.sql, Platform.OS === 'android' ? query.args.map(_escapeBlob) : query.args];
43}
44
45function _deserializeResultSet(nativeResult): ResultSet | ResultSetError {
46  let [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult;
47  // TODO: send more structured error information from the native module so we can better construct
48  // a SQLException object
49  if (errorMessage !== null) {
50    return { error: new Error(errorMessage) } as ResultSetError;
51  }
52
53  return {
54    insertId,
55    rowsAffected,
56    rows: rows.map(row => zipObject(columns, row)),
57  };
58}
59
60function _escapeBlob<T>(data: T): T {
61  if (typeof data === 'string') {
62    /* eslint-disable no-control-regex */
63    return data
64      .replace(/\u0002/g, '\u0002\u0002')
65      .replace(/\u0001/g, '\u0001\u0002')
66      .replace(/\u0000/g, '\u0001\u0001') as any;
67    /* eslint-enable no-control-regex */
68  } else {
69    return data;
70  }
71}
72
73const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase);
74
75function addExecMethod(db: any): WebSQLDatabase {
76  db.exec = (queries: Query[], readOnly: boolean, callback: SQLiteCallback): void => {
77    db._db.exec(queries, readOnly, callback);
78  };
79  return db;
80}
81
82export function openDatabase(
83  name: string,
84  version: string = '1.0',
85  description: string = name,
86  size: number = 1,
87  callback?: (db: WebSQLDatabase) => void
88): WebSQLDatabase {
89  if (name === undefined) {
90    throw new TypeError(`The database name must not be undefined`);
91  }
92  const db = _openExpoSQLiteDatabase(name, version, description, size, callback);
93  const dbWithExec = addExecMethod(db);
94  return dbWithExec;
95}
96