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