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