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