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