commit 9a25ad99a1ff893747bbd5dfcd8c882bffd3a914 Author: AJ ONeal Date: Thu Aug 27 21:33:46 2015 -0600 initial commit diff --git a/config.test.js b/config.test.js new file mode 100644 index 0000000..2c257ca --- /dev/null +++ b/config.test.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + // crypto.randomBytes(16).toString('hex'); + key: '1892d335081d8d346e556c9c3c8ff2c3' +, filename: '/tmp/dbwrap.test.sqlcipher' +}; diff --git a/lib/dbwrap.js b/lib/dbwrap.js new file mode 100644 index 0000000..aa1f105 --- /dev/null +++ b/lib/dbwrap.js @@ -0,0 +1,345 @@ +'use strict'; + +function wrap(db, dir) { + // TODO if I put a failure right here, + // why doesn't the unhandled promise rejection fire? + var PromiseA = require('bluebird'); + var promises = []; + var dbsMap = {}; + var arr = true; + + function lowerFirst(str) { + return str.charAt(0).toLowerCase() + str.slice(1); + } + + function snakeCase(str) { + return lowerFirst(str).replace( + /([A-Z])/g + , function ($1) { + return "_" + $1.toLowerCase(); + } + ); + } + + function camelCase(str) { + return str.replace( + /_([a-z])/g + , function (g) { + return g[1].toUpperCase(); + }); + } + + function upperCamelCase(str) { + // TODO handle UTF-8 properly (use codePointAt, don't use slice) + return camelCase(str).charAt(0).toUpperCase() + str.slice(1); + } + + function createTable(opts) { + var DB = {}; + var tablename = db.escape(snakeCase(opts.tablename) || 'data'); + var idname = db.escape(snakeCase(opts.idname) || 'id'); + + db = PromiseA.promisifyAll(db); + + if (opts && opts.verbose || db.verbose) { + console.log('Getting Verbose up in here'); + db.on('trace', function (str) { + console.log('SQL:', str); + }); + + db.on('profile', function (sql, ms) { + console.log('Profile:', ms); + }); + } + + function simpleParse(row) { + var obj; + + if (!row) { + return null; + } + + if (row.json) { + obj = JSON.parse(row.json); + } else { + obj = {}; + } + + obj[idname] = row[idname]; + + return obj; + } + + function simpleMap(rows) { + if (!rows) { + return []; + } + + var results = rows.map(function (row, i) { + // set up for garbage collection + rows[i] = null; + + var obj; + + if (row.json) { + obj = JSON.parse(row.json); + } else { + obj = {}; + } + + obj[idname] = row[idname]; + }); + // set up for garbage collection + rows.length = 0; + rows = null; + + return results; + } + + DB.find = function (opts) { + var sql = 'SELECT * FROM ' + tablename + ' WHERE '; + + Object.keys(opts).forEach(function (key, i) { + if (i !== 0) { + sql += 'AND '; + } + sql += db.escape(snakeCase(key)) + ' ' + db.escape(opts[key]); + }); + + return db.allAsync("SELECT * FROM " + tablename + " " + sql, []).then(simpleMap); + }; + + DB.get = function (id) { + var sql = "SELECT * FROM " + tablename + " WHERE " + idname + " = ?"; + var values = [id]; + + return db.getAsync(sql, values).then(function (rows) { + if (Array.isArray(rows)) { + if (!rows.length) { + return null; + } + + return rows[0] || null; + } + + return rows; + }).then(simpleParse); + }; + + DB.upsert = function (id, data) { + return DB.set(id, data).then(function (result) { + var success = result.changes >= 1; + + if (success) { + return result; + } + + return DB.create(id, data); + }); + }; + + DB.save = function (data) { + if (!data[idname]) { + // NOTE saving the id both in the object and the id for now + var UUID = require('node-uuid'); + data[idname] = UUID.v4(); + return DB.create(data[idname], data).then(function (/*stats*/) { + //data._rowid = stats.id; + return data; + }); + } + + return DB.set(data[idname], data).then(function (result) { + var success = result.changes >= 1; + + if (success) { + return result; + } + }); + }; + + DB.create = function (id, data) { + var json = JSON.stringify(data); + + return new PromiseA(function (resolve, reject) { + var sql = "INSERT INTO " + tablename + " (" + idname + ", json) VALUES (?, ?)"; + var values = [id, json]; + + db.run(sql, values, function (err) { + if (err) { + reject(err); + return; + } + + // NOTE changes is 1 even if the value of the updated record stays the same + // (PostgreSQL would return 0 in that case) + // thus if changes is 0 then it failed, otherwise it succeeded + /* + console.log('[log db wrapper insert]'); + console.log(this); // sql, lastID, changes + console.log(this.sql); + console.log('insert lastID', this.lastID); // sqlite's internal rowId + console.log('insert changes', this.changes); + */ + + //this.id = id; + resolve(this); + }); + }); + }; + + DB.set = function (id, obj) { + var json = JSON.stringify(obj); + var data = JSON.parse(json); + + return new PromiseA(function (resolve, reject) { + var sql; + var fieldable = []; + var vals = []; + + (opts.indices || []).forEach(function (col) { + if ('string' === typeof col) { + col = { name: col, type: 'TEXT' }; + } + if (!col.type) { + col.type = 'TEXT'; + } + + var val = data[camelCase(col.name)]; + + //if (col.name in data) + if ('undefined' !== typeof val) { + /* + fieldable.push( + db.escape(snakeCase(col.name)) + + " = '" + db.escape(val) + "'" + ); + */ + fieldable.push(db.escape(snakeCase(col.name)) + " = ?"); + vals.push(val); + } + + delete data[col.name]; + }); + + json = JSON.stringify(data); + fieldable.push("json = ?"); + //fieldable.push("json = '" + db.escape(json) + "'"); + vals.push(json); + + vals.push(id); + sql = "UPDATE " + tablename + " SET " + fieldable.join(', ') + " WHERE " + idname + " = ?"; + + while (vals.length) { + sql = sql.replace(/\?/, "'" + db.escape(vals.shift()) + "'"); + } + + console.log('[debug] sql:', sql); + db.run(sql, vals, function (err) { + if (err) { + reject(err); + return; + } + + // it isn't possible to tell if the update succeeded or failed + // only if the update resulted in a change or not + /* + console.log('[log db wrapper set]'); + console.log(this); // sql, lastID, changes + console.log(this.sql); + console.log('update lastID', this.lastID); // always 0 (except on INSERT) + console.log('update changes', this.changes); + */ + + resolve(this); + }); + }); + }; + + DB.destroy = function (id) { + if ('object' === typeof id) { + id = id[idname]; + } + + return new PromiseA(function (resolve, reject) { + var sql = "DELETE FROM " + tablename + " WHERE " + idname + " = ?"; + var values = [id]; + + db.run(sql, values, function (err) { + if (err) { + reject(err); + return; + } + + // it isn't possible to tell if the update succeeded or failed + // only if the update resulted in a change or not + /* + console.log('[log db wrapper delete]'); + console.log(this); // sql, lastID, changes + console.log(this.sql); + console.log('delete lastID', this.lastID); // always 0 (except on INSERT) + console.log('delete changes', this.changes); + */ + + resolve(this); + }); + }); + }; + + DB._db = db; + + return new PromiseA(function (resolve, reject) { + var indexable = [idname + ' TEXT']; + var sql; + + (opts.indices || []).forEach(function (col) { + if ('string' === typeof col) { + col = { name: col, type: 'TEXT' }; + } + if (!col.type) { + col.type = 'TEXT'; + } + indexable.push( + db.escape(snakeCase(col.name)) + + ' ' + db.escape(col.type) + ); + }); + indexable.push('json TEXT'); + + sql = "CREATE TABLE IF NOT EXISTS '" + tablename + "' " + + "(" + indexable.join(', ') + ", PRIMARY KEY(" + idname + "))" + ; + + db.runAsync(sql).then(function () { resolve(DB); }, reject); + }); + } + + if (!Array.isArray(dir)) { + arr = false; + dir = [dir]; + } + + dir.forEach(function (opts) { + promises.push(createTable(opts).then(function (dbw) { + var tablename = (opts.tablename || 'data'); + + tablename = upperCamelCase(tablename); + + dbsMap[tablename] = dbw; + + return dbw; + })); + }); + + dbsMap.sql = db; + + return PromiseA.all(promises).then(function (dbs) { + if (!arr) { + return dbs[0]; + } + + return dbsMap; + }); +} + +module.exports.wrap = wrap; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4fb0aef --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "dbwrap", + "version": "1.0.0", + "description": "The world's worst database abstraction layer - just in case the others didn't suck enough", + "main": "lib/dbwrap", + "scripts": { + "test": "node tests/dbwrap" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@bitbucket.org/daplie/dbwrap.git" + }, + "author": "AJ ONeal (http://coolaj86.com/)", + "license": "Apache-2.0", + "homepage": "https://bitbucket.org/daplie/dbwrap#readme", + "dependencies": { + "bluebird": "^2.9.34" + } +} diff --git a/tests/dbwrap.js b/tests/dbwrap.js new file mode 100644 index 0000000..a8f77ec --- /dev/null +++ b/tests/dbwrap.js @@ -0,0 +1,61 @@ +'use strict'; + +var PromiseA = require('bluebird').Promise; + +function testDb(DB) { + return PromiseA.resolve(DB).then(function (DB) { + var data = { secret: 'super secret', verifiedAt: 1437207288791 }; + //return DB.set('aj@the.dj', data) + //return DB.set('john.doe@email.com', data) + // return DB.upsert('jane.doe@email.com', data) + return DB.upsert('jane.doe@email.com', data).then(function () { + console.info('[PASS] added user'); + }); + + /* + return DB.create('john.doe@email.com', data).then(function () { + console.log('added user'); + }); + */ + + // need to 'DELETE FROM authn;' first + return DB.get('john.doe@email.com').then(function (user) { + if (user) { + console.info('user', user); + return; + } + + //var data = { secret: 'super secret', verifiedAt: Date.now() }; + var data = { secret: 'super secret', verifiedAt: 1437207288790 }; + return DB.create('john.doe@email.com', data).then(function () { + console.info('added user'); + }); + + }); + }).then(function () {}, function (err) { + // code SQLITE_CONSTRAINT + // errno 19 + + console.error('[ERROR] during test'); + //console.error(Object.keys(err)); // errno, code + console.error(err); + }); +} + +function run(/*isMaster*/) { + require('./setup').run().then(testDb); + + /* + if (require.main === module) { + // crypto.randomBytes(16).toString('hex'); + create({ + key: '1892d335081d8d346e556c9c3c8ff2c3' + , bits: 128 + , filename: '/tmp/authn.sqlcipher' + }).then(function (DB) { + }); + } + */ +} + +run(); diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 0000000..1c76a83 --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,32 @@ +function run(opts) { + var config = require('../config.test.js'); + var sqlite3 = require('sqlite3-cluster'); + var wrap = require('../lib/dbwrap'); + + var promise = sqlite3.create({ + standalone: true + , bits: 128 + , filename: config.filename + , verbose: false + }); + + return promise.then(function (db) { + return db.init({ bits: 128, key: config.key }); + }).then(function (db) { + return wrap.wrap(db, Array.isArray(opts) && opts || { idname: 'uuid', tablename: opts && opts.tablename || 'authn' }); + }); + + /* + if (require.main === module) { + create({ + key: '1892d335081d8d346e556c9c3c8ff2c3' + , bits: 128 + , filename: '/tmp/authn.sqlcipher' + }).then(function (DB) { + }); + } + */ +} + +module.exports = run; +module.exports.run = run;