diff --git a/.travis.yml b/.travis.yml index cff8ae2..2c7e40f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,3 +9,4 @@ services: - mysql before_script: - mysql -e 'create database sworm;' + - export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start diff --git a/index.js b/index.js index 69eec08..db85144 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var pgDriver = require("./pgDriver"); var mysqlDriver = require("./mysqlDriver"); var oracleDriver = require("./oracleDriver"); var sqliteDriver = require("./sqliteDriver"); +var websqlDriver = require("./websqlDriver"); var debug = require("debug")("sworm"); var debugResults = require("debug")("sworm:results"); var redactConfig = require('./redactConfig'); @@ -459,7 +460,8 @@ exports.db = function(config) { pg: pgDriver, mysql: mysqlDriver, oracle: oracleDriver, - sqlite: sqliteDriver + sqlite: sqliteDriver, + websql: websqlDriver }[this.config.driver]; if (!driver) { @@ -568,8 +570,10 @@ exports.escape = function(value) { } function configFromUrl(url) { + var isBrowser = typeof window !== 'undefined' + var parsedUrl = urlUtils.parse(url) - var protocol = parsedUrl.protocol? parsedUrl.protocol.replace(/:$/, ''): 'sqlite' + var protocol = parsedUrl.protocol? parsedUrl.protocol.replace(/:$/, ''): (isBrowser? 'websql': 'sqlite') var driver = { postgres: 'pg', file: 'sqlite' diff --git a/package.json b/package.json index b0d9b3d..26fc6e3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "devDependencies": { "chai": "3.5.0", "chai-as-promised": "5.3.0", + "electron": "1.6.2", + "electron-mocha": "3.3.0", "es6-promise": "3.2.1", "fs-promise": "0.5.0", "mocha": "3.2.0", @@ -23,7 +25,7 @@ "sqlite3": "3.1.8" }, "scripts": { - "test": "mocha" + "test": "mocha && electron-mocha --renderer test/browser" }, "repository": { "type": "git", diff --git a/readme.md b/readme.md index 0677da5..14e831e 100644 --- a/readme.md +++ b/readme.md @@ -7,6 +7,7 @@ A very lightweight **write only** Node.js ORM, with support for: * MySQL * Oracle DB * Sqlite 3 +* Browser Web SQL ## NPM @@ -20,6 +21,8 @@ Then install a database driver, one of: npm install oracledb npm install sqlite3 +There's no need to install a driver for Web SQL, sworm will pick it up from the `window` object. + See [sworm](https://www.npmjs.org/package/sworm) in NPM. ## Write Only? @@ -158,7 +161,7 @@ sworm.db(url) * `url`, see urls for databases in respective section below - * `options.driver`, one of `'mssql'`, `'mysql'`, `'pg'`, `'oracle'` or `'sqlite'`. + * `options.driver`, one of `'mssql'`, `'mysql'`, `'pg'`, `'oracle'`, `'sqlite'` or `'websql'`. * `options.config` see configuration for databases in respective section below * `url` a connection URL, the following are supported @@ -301,6 +304,25 @@ sworm.db(url) } ``` +* **websql** + + URL: `websql:///db-name` or `db-name` + + ```js + { + driver: 'websql', + config: { + name: 'db-name', + + // the `openDatabase` function to connect to the DB, defaulting to `window.openDatabase` + openDatabase: window.openDatabase, + + // dababase size, defaulting to 5M + size: 5 * 1024 * 1024 + } + } + ``` + ### Close Close the connection after use: @@ -326,6 +348,7 @@ There are various schemes you can use: * `sworm:pg` exact query passed to postgres * `sworm:oracle` exact query passed to oracle * `sworm:sqlite` exact query passed to sqlite3 +* `sworm:websql` exact query passed to websql ## Models diff --git a/test/browser/websqlSpec.js b/test/browser/websqlSpec.js new file mode 100644 index 0000000..d434853 --- /dev/null +++ b/test/browser/websqlSpec.js @@ -0,0 +1,85 @@ +var describeDatabase = require('../describeDatabase'); +var sworm = require('../..') +var expect = require('chai').expect + +var database = { + createTables: function(db, tables) { + function createTable(name, sql) { + tables.push(name); + return db.query(sql); + } + + return createTable("people", + 'create table if not exists people (id integer primary key, name varchar(50) NOT NULL, address_id integer NULL, photo blob null)' + ).then(function() { + return createTable("people_addresses", + 'create table if not exists people_addresses(address_id integer NOT NULL, person_id integer NOT NULL, rating integer NULL)' + ); + }).then(function() { + return createTable("addresses", + 'create table if not exists addresses(id integer primary key, address varchar(50) NOT NULL)' + ); + }).then(function() { + return createTable("people_weird_id", + 'create table if not exists people_weird_id(weird_id integer primary key, name varchar(50) NULL, address_weird_id integer NULL)' + ); + }).then(function() { + return createTable("people_explicit_id", + 'create table if not exists people_explicit_id(id integer NOT NULL, name varchar(50) NOT NULL)' + ); + }); + }, + + "true": 1, + "false": 0, + + clean: function(records) { + return records; + }, + + driverModuleName: "websql", + + transactions: false, + + noModule: true +}; + +var config = { + driver: "websql", + config: { + name: 'test' + } +}; + +describeDatabase("websql", config, database, function () { + describe('connection', function () { + it('can connect with string for DB name', function () { + var db = sworm.db('test') + return db.query('select * from people') + }) + + it('can connect with URL', function () { + var db = sworm.db('websql:///test') + return db.query('select * from people') + }) + + it('can connect with openDatabase function', function () { + var called = false + + var db = sworm.db({ + driver: 'websql', + config: { + openDatabase: function() { + called = true + return window.openDatabase.apply(window, arguments) + }, + name: 'test' + } + }) + + return db.query('select * from people').then(function () { + expect(called).to.be.true + }) + }) + }) +}) diff --git a/test/describeDatabase.js b/test/describeDatabase.js index 652e802..a1872db 100644 --- a/test/describeDatabase.js +++ b/test/describeDatabase.js @@ -10,23 +10,25 @@ require('es6-promise').polyfill(); module.exports = function(name, config, database, otherTests) { describe(name, function() { - describe("missing modules", function() { - var moduleName = __dirname + "/../node_modules/" + database.driverModuleName; + if (!database.noModule) { + describe("missing modules", function() { + var moduleName = __dirname + "/../node_modules/" + database.driverModuleName; - beforeEach(function() { - return fs.rename(moduleName, moduleName + ".missing"); - }); + beforeEach(function() { + return fs.rename(moduleName, moduleName + ".missing"); + }); - afterEach(function() { - return fs.rename(moduleName + ".missing", moduleName); - }); + afterEach(function() { + return fs.rename(moduleName + ".missing", moduleName); + }); - it("throws an exception if the driver module is not present", function() { - return expect(function() { - sworm.db(config).connect(); - }).to.throw("npm install " + database.driverModuleName); + it("throws an exception if the driver module is not present", function() { + return expect(function() { + sworm.db(config).connect(); + }).to.throw("npm install " + database.driverModuleName); + }); }); - }); + } context('with a database', function () { before(function () { @@ -198,116 +200,129 @@ module.exports = function(name, config, database, otherTests) { }); }); - describe('transactions', function () { - beforeEach(function () { - if (database.setAutoCommit) { - database.setAutoCommit(false); - } - }); + if (!database.hasOwnProperty('transactions') || database.transactions != false) { + describe('transactions', function () { + beforeEach(function () { + if (database.setAutoCommit) { + database.setAutoCommit(false); + } + }); - afterEach(function () { - if (database.setAutoCommit) { - database.setAutoCommit(true); - } - }); + afterEach(function () { + if (database.setAutoCommit) { + database.setAutoCommit(true); + } + }); - describe('rollback', function () { - describe('explicit', function () { - it('rolls back when rollback is called', function () { - return db.begin().then(function () { - var bob = person({ name: 'bob' }); - return bob.save().then(function () { - return db.query('select * from people'); - }).then(function (people) { - expect(people).to.eql([ - { - id: bob.id, - name: 'bob', - address_id: null, - photo: null - } - ]); - }).then(function() { - return db.rollback(); - }).then(function() { - return db.query('select * from people'); - }).then(function(people) { - expect(people).to.eql([ - ]); + describe('rollback', function () { + describe('explicit', function () { + it('rolls back when rollback is called', function () { + return db.begin().then(function () { + var bob = person({ name: 'bob' }); + return bob.save().then(function () { + return db.query('select * from people'); + }).then(function (people) { + expect(people).to.eql([ + { + id: bob.id, + name: 'bob', + address_id: null, + photo: null + } + ]); + }).then(function() { + return db.rollback(); + }).then(function() { + return db.query('select * from people'); + }).then(function(people) { + expect(people).to.eql([ + ]); + }); }); }); }); - }); - describe('scoped', function () { - it('rolls back if the transaction scope throws an exception', function () { - return expect(db.transaction(function () { - var bob = person({ name: 'bob' }); - return bob.save().then(function() { + describe('scoped', function () { + it('rolls back if the transaction scope throws an exception', function () { + return expect(db.transaction(function () { + var bob = person({ name: 'bob' }); + return bob.save().then(function() { + return db.query('select * from people'); + }).then(function(people) { + expect(people).to.eql([ + { + id: bob.id, + name: 'bob', + address_id: null, + photo: null + } + ]); + + throw new Error('uh oh'); + }); + })).to.be.rejectedWith('uh oh').then(function() { return db.query('select * from people'); }).then(function(people) { expect(people).to.eql([ - { - id: bob.id, - name: 'bob', - address_id: null, - photo: null - } ]); - - throw new Error('uh oh'); }); - })).to.be.rejectedWith('uh oh').then(function() { - return db.query('select * from people'); - }).then(function(people) { - expect(people).to.eql([ - ]); }); }); }); - }); - describe('commit', function () { - describe('explicit', function () { - it('makes changes after commit is called', function () { - return db.begin().then(function () { - var bob = person({ name: 'bob' }); - return bob.save().then(function() { - return db.query('select * from people'); - }).then(function(people) { - expect(people).to.eql([ - { - id: bob.id, - name: 'bob', - address_id: null, - photo: null - } - ]); - }).then(function() { - return db.commit(); - }).then(function() { - return db.query('select * from people'); - }).then(function(people) { - expect(people).to.eql([ - { - id: bob.id, - name: 'bob', - address_id: null, - photo: null - } - ]); + describe('commit', function () { + describe('explicit', function () { + it('makes changes after commit is called', function () { + return db.begin().then(function () { + var bob = person({ name: 'bob' }); + return bob.save().then(function() { + return db.query('select * from people'); + }).then(function(people) { + expect(people).to.eql([ + { + id: bob.id, + name: 'bob', + address_id: null, + photo: null + } + ]); + }).then(function() { + return db.commit(); + }).then(function() { + return db.query('select * from people'); + }).then(function(people) { + expect(people).to.eql([ + { + id: bob.id, + name: 'bob', + address_id: null, + photo: null + } + ]); + }); }); }); }); - }); - describe('scoped', function () { - it('makes changes after commit is called', function () { - var bob; + describe('scoped', function () { + it('makes changes after commit is called', function () { + var bob; - return db.transaction(function () { - bob = person({ name: 'bob' }); - return bob.save().then(function() { + return db.transaction(function () { + bob = person({ name: 'bob' }); + return bob.save().then(function() { + return db.query('select * from people'); + }).then(function(people) { + expect(people).to.eql([ + { + id: bob.id, + name: 'bob', + address_id: null, + photo: null + } + ]); + }); + }).then(function() { return db.query('select * from people'); }).then(function(people) { expect(people).to.eql([ @@ -319,22 +334,19 @@ module.exports = function(name, config, database, otherTests) { } ]); }); - }).then(function() { - return db.query('select * from people'); - }).then(function(people) { - expect(people).to.eql([ - { - id: bob.id, - name: 'bob', - address_id: null, - photo: null - } - ]); }); }); }); }); - }); + } else { + describe('no transactions', function () { + it('throws when asked to create a new transaction', function () { + expect(function () { + db.begin() + }).to.throw('transactions are not supported') + }) + }) + } describe("concurrency", function() { it("can insert multiple rows, maintaining correct IDs", function() { diff --git a/websqlDriver.js b/websqlDriver.js new file mode 100644 index 0000000..d1dcb47 --- /dev/null +++ b/websqlDriver.js @@ -0,0 +1,103 @@ +var debug = require('debug')('sworm:websql'); +var paramRegex = require('./paramRegex') +var urlUtils = require('url') + +module.exports = function() { + return { + query: function(query, params, options) { + var self = this; + var paramList = [] + + if (params) { + query = query.replace(paramRegex, function(_, paramName) { + if (!params.hasOwnProperty(paramName)) { + throw new Error('no such parameter @' + paramName); + } else { + paramList.push(params[paramName]); + } + return '?'; + }); + } + + return new Promise(function (fulfil, reject) { + debug(query, paramList); + self.connection.transaction(function(tx) { + tx.executeSql(query, paramList, function(tx, result){ + if (options.statement || options.insert) { + var r = {} + + if (options.statement) { + r.changes = result.rowsAffected + } + + if (options.insert) { + r.id = result.insertId + } + + fulfil(r) + } else { + var results = [] + for (var i = 0; i