From d99e9d2c335eb166cc7ab9ad29c96dfa1f9602a0 Mon Sep 17 00:00:00 2001
From: Alpha <alpha0010@users.noreply.github.com>
Date: Mon, 11 Mar 2019 23:46:05 -0400
Subject: [PATCH] Draft simple API

---
 .npmignore      |   4 +-
 babel.config.js |   3 +
 package.json    |   6 +-
 simple.js       | 131 ++++++++++++++++++++++++++
 simple.test.js  | 242 ++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 384 insertions(+), 2 deletions(-)
 create mode 100644 babel.config.js
 create mode 100644 simple.js
 create mode 100644 simple.test.js

diff --git a/.npmignore b/.npmignore
index 34e4c963..ef4e9b7f 100644
--- a/.npmignore
+++ b/.npmignore
@@ -81,4 +81,6 @@ gradle-app.setting
 # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
 !gradle-wrapper.jar
 
-instructions/
\ No newline at end of file
+instructions/
+
+*.test.js
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 00000000..34d08766
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  presets: ['@babel/preset-flow']
+};
diff --git a/package.json b/package.json
index 5a2bfa99..ce2bfc52 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
   "description": "SQLite3 bindings for React Native (Android & iOS)",
   "main": "sqlite.js",
   "scripts": {
-    "test": "echo \"Error: no test specified yet\" && exit 1"
+    "test": "jest"
   },
   "repository": {
     "type": "git",
@@ -39,5 +39,9 @@
     "ios": {
       "project": "src/ios/SQLite.xcodeproj"
     }
+  },
+  "devDependencies": {
+    "@babel/preset-flow": "^7.0.0",
+    "jest": "^24.4.0"
   }
 }
diff --git a/simple.js b/simple.js
new file mode 100644
index 00000000..cc7098de
--- /dev/null
+++ b/simple.js
@@ -0,0 +1,131 @@
+/**
+ * @flow strict-local
+ */
+
+'use strict';
+
+const { NativeModules } = require('react-native');
+const { SQLite } = NativeModules;
+
+export type DataType = number | string;
+
+export type QueryFailResult = {
+  qid: number,
+  result: {
+    message: string
+  },
+  type: 'error'
+};
+
+export type QuerySuccessResult<T> = {
+  qid: number,
+  result: {
+    insertId?: number,
+    rows?: T[],
+    rowsAffected?: number
+  },
+  type: 'success'
+};
+
+export type QueryResult<T> = QueryFailResult | QuerySuccessResult<T>;
+
+class Database {
+  lastQueryID: number;
+  name: string;
+
+  constructor(name: string) {
+    this.lastQueryID = 0;
+    this.name = name;
+  }
+
+  /**
+   * Get all results from the query.
+   */
+  async all<T>(sql: string, params?: DataType[]): Promise<T[]> {
+    const results = await this.executeBatch<T>([{ sql, params }]);
+    const result = results[0];
+    if (result.type === 'success') {
+      if (result.result.rows == null) {
+        // Statement was not a SELECT.
+        return [];
+      } else {
+        return result.result.rows;
+      }
+    } else {
+      throw new Error(result.result.message);
+    }
+  }
+
+  /**
+   * Close the database.
+   */
+  async close(): Promise<void> {
+    return new Promise((resolve, reject) => {
+      SQLite.close({ path: this.name }, () => resolve(), reject);
+    });
+  }
+
+  /**
+   * Execute a statement.
+   */
+  async exec(sql: string, params?: DataType[]): Promise<void> {
+    await this.all(sql, params);
+  }
+
+  /**
+   * Execute a batch of queries, returning the status and output of each.
+   */
+  async executeBatch<T>(
+    queries: { sql: string, params?: DataType[] }[]
+  ): Promise<QueryResult<T>[]> {
+    if (queries.length === 0) {
+      return [];
+    }
+
+    const executes: { qid: number, sql: string, params: ?(DataType[]) }[] = [];
+    for (const query of queries) {
+      this.lastQueryID += 1;
+      executes.push({
+        qid: this.lastQueryID,
+        sql: query.sql,
+        params: query.params == null ? [] : query.params
+      });
+    }
+
+    return new Promise((resolve, reject) => {
+      SQLite.executeSqlBatch(
+        {
+          dbargs: { dbname: this.name },
+          executes
+        },
+        resolve,
+        reject
+      );
+    });
+  }
+
+  /**
+   * Get the first result from the query.
+   */
+  async get<T>(sql: string, params?: DataType[]): Promise<?T> {
+    const rows = await this.all<T>(sql, params);
+    return rows.length > 0 ? rows[0] : null;
+  }
+}
+
+export type { Database };
+
+/**
+ * Open the database.
+ */
+async function open(filename: string): Promise<Database> {
+  return new Promise((resolve, reject) => {
+    SQLite.open(
+      { name: filename, dblocation: 'nosync' },
+      () => resolve(new Database(filename)),
+      reject
+    );
+  });
+}
+
+module.exports = { open };
diff --git a/simple.test.js b/simple.test.js
new file mode 100644
index 00000000..f2f4bcbc
--- /dev/null
+++ b/simple.test.js
@@ -0,0 +1,242 @@
+/* global beforeEach, jest, test, expect */
+/* @flow strict-local */
+
+'use strict';
+
+const { NativeModules } = require('react-native');
+const sqlite = require('./simple');
+
+jest.mock(
+  'react-native',
+  () => {
+    return {
+      NativeModules: {
+        SQLite: {
+          close: jest.fn((args, success, error) => {
+            success('database removed');
+          }),
+
+          executeSqlBatch: jest.fn(),
+
+          open: jest.fn((args, success, error) => {
+            success('Database opened');
+          })
+        }
+      }
+    };
+  },
+  { virtual: true }
+);
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('Open database', async () => {
+  await sqlite.open('filename');
+
+  expect(NativeModules.SQLite.open).toHaveBeenCalledWith(
+    { name: 'filename', dblocation: expect.any(String) },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});
+
+test('Get all results', async () => {
+  NativeModules.SQLite.executeSqlBatch.mockImplementationOnce(
+    (args, success, error) => {
+      success([
+        {
+          qid: 1,
+          result: { rows: [{ id: 1, val: 'test' }, { id: 2, val: 'case' }] },
+          type: 'success'
+        }
+      ]);
+    }
+  );
+
+  // Execute.
+  const db = await sqlite.open('filename');
+  const results = await db.all('SELECT id, val FROM foo WHERE bar = ?', [
+    'baz'
+  ]);
+
+  expect(results).toEqual([{ id: 1, val: 'test' }, { id: 2, val: 'case' }]);
+
+  expect(NativeModules.SQLite.executeSqlBatch).toHaveBeenCalledWith(
+    {
+      dbargs: { dbname: 'filename' },
+      executes: [
+        {
+          qid: 1,
+          sql: 'SELECT id, val FROM foo WHERE bar = ?',
+          params: ['baz']
+        }
+      ]
+    },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});
+
+test('Execute statement', async () => {
+  NativeModules.SQLite.executeSqlBatch.mockImplementationOnce(
+    (args, success, error) => {
+      success([
+        {
+          qid: 1,
+          result: { rowsAffected: 0 },
+          type: 'success'
+        }
+      ]);
+    }
+  );
+
+  // Execute.
+  const db = await sqlite.open('filename');
+  const results = await db.exec('BEGIN');
+
+  expect(results).toBeUndefined();
+
+  expect(NativeModules.SQLite.executeSqlBatch).toHaveBeenCalledWith(
+    {
+      dbargs: { dbname: 'filename' },
+      executes: [
+        {
+          qid: 1,
+          sql: 'BEGIN',
+          params: []
+        }
+      ]
+    },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});
+
+test('Execute invalid statement', async () => {
+  NativeModules.SQLite.executeSqlBatch.mockImplementationOnce(
+    (args, success, error) => {
+      success([
+        {
+          qid: 1,
+          result: {
+            message:
+              'near "INVALID": syntax error (code 1 SQLITE_ERROR): , while compiling: INVALID'
+          },
+          type: 'error'
+        }
+      ]);
+    }
+  );
+
+  // Execute.
+  const db = await sqlite.open('filename');
+  await expect(db.exec('INVALID')).rejects.toThrow(
+    'near "INVALID": syntax error (code 1 SQLITE_ERROR): , while compiling: INVALID'
+  );
+
+  expect(NativeModules.SQLite.executeSqlBatch).toHaveBeenCalledWith(
+    {
+      dbargs: { dbname: 'filename' },
+      executes: [
+        {
+          qid: 1,
+          sql: 'INVALID',
+          params: []
+        }
+      ]
+    },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});
+
+test('Execute empty batch', async () => {
+  const db = await sqlite.open('filename');
+  const results = await db.executeBatch([]);
+
+  expect(results).toEqual([]);
+  expect(NativeModules.SQLite.executeSqlBatch).not.toHaveBeenCalled();
+});
+
+test('Get item', async () => {
+  NativeModules.SQLite.executeSqlBatch.mockImplementationOnce(
+    (args, success, error) => {
+      success([
+        {
+          qid: 1,
+          result: { rows: [{ id: 1, val: 'test' }] },
+          type: 'success'
+        }
+      ]);
+    }
+  );
+
+  // Execute.
+  const db = await sqlite.open('filename');
+  const result = await db.get('SELECT id, val FROM foo WHERE id = ?', [1]);
+
+  expect(result).toEqual({ id: 1, val: 'test' });
+
+  expect(NativeModules.SQLite.executeSqlBatch).toHaveBeenCalledWith(
+    {
+      dbargs: { dbname: 'filename' },
+      executes: [
+        {
+          qid: 1,
+          sql: 'SELECT id, val FROM foo WHERE id = ?',
+          params: [1]
+        }
+      ]
+    },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});
+
+test('Get item no results', async () => {
+  NativeModules.SQLite.executeSqlBatch.mockImplementationOnce(
+    (args, success, error) => {
+      success([
+        {
+          qid: 1,
+          result: { rows: [] },
+          type: 'success'
+        }
+      ]);
+    }
+  );
+
+  // Execute.
+  const db = await sqlite.open('filename');
+  const result = await db.get('SELECT id, val FROM foo WHERE id = ?', [1]);
+
+  expect(result).toBeNull();
+
+  expect(NativeModules.SQLite.executeSqlBatch).toHaveBeenCalledWith(
+    {
+      dbargs: { dbname: 'filename' },
+      executes: [
+        {
+          qid: 1,
+          sql: 'SELECT id, val FROM foo WHERE id = ?',
+          params: [1]
+        }
+      ]
+    },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});
+
+test('Close database', async () => {
+  const db = await sqlite.open('filename');
+  await db.close();
+
+  expect(NativeModules.SQLite.close).toHaveBeenCalledWith(
+    { path: 'filename' },
+    expect.any(Function),
+    expect.any(Function)
+  );
+});