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 = { + qid: number, + result: { + insertId?: number, + rows?: T[], + rowsAffected?: number + }, + type: 'success' +}; + +export type QueryResult = QueryFailResult | QuerySuccessResult; + +class Database { + lastQueryID: number; + name: string; + + constructor(name: string) { + this.lastQueryID = 0; + this.name = name; + } + + /** + * Get all results from the query. + */ + async all(sql: string, params?: DataType[]): Promise { + const results = await this.executeBatch([{ 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 { + return new Promise((resolve, reject) => { + SQLite.close({ path: this.name }, () => resolve(), reject); + }); + } + + /** + * Execute a statement. + */ + async exec(sql: string, params?: DataType[]): Promise { + await this.all(sql, params); + } + + /** + * Execute a batch of queries, returning the status and output of each. + */ + async executeBatch( + queries: { sql: string, params?: DataType[] }[] + ): Promise[]> { + 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(sql: string, params?: DataType[]): Promise { + const rows = await this.all(sql, params); + return rows.length > 0 ? rows[0] : null; + } +} + +export type { Database }; + +/** + * Open the database. + */ +async function open(filename: string): Promise { + 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) + ); +});