From b51e2dbdf15b0e2894240621e34797f6f7892608 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 25 Oct 2016 05:51:16 -0500 Subject: [PATCH 01/22] :snowflake: basic setup #3 --- .editorconfig | 8 ++++++++ .eslintignore | 1 + .gitignore | 6 ++++++ package.json | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 package.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1923d41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..4ebc8ae --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +coverage diff --git a/.gitignore b/.gitignore index 5148e52..daa5a20 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,9 @@ jspm_packages # Optional REPL history .node_repl_history + +# goodparts symlink +.eslintrc.js + +# environment variables +*.env diff --git a/package.json b/package.json new file mode 100644 index 0000000..22d5a1d --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "abase-db", + "version": "1.0.0", + "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", + "main": "lib/index.js", + "devDependencies": { + "goodparts": "^1.1.0", + "istanbul": "^0.4.5", + "pre-commit": "^1.1.3", + "tape": "^4.6.2" + }, + "dependencies": { + "env2": "^2.1.1" + }, + "scripts": { + "test": "tape ./test/**/*.test.js", + "lint": "node_modules/.bin/goodparts .", + "lint:fix": "node_modules/.bin/goodparts . --fix", + "cover": "node_modules/.bin/istanbul cover node_modules/.bin/tape ./test/*.test.js", + "check-coverage": "node_modules/.bin/istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100", + "start": "node lib/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dwyl/joi-postgresql.git" + }, + "author": "@eliascodes && @jrans", + "license": "ISC", + "bugs": { + "url": "https://github.com/dwyl/joi-postgresql/issues" + }, + "homepage": "https://github.com/dwyl/joi-postgresql#readme" +} From 88b53682d15cffbcf99911bb56c74e4ec157d68a Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 25 Oct 2016 05:52:32 -0500 Subject: [PATCH 02/22] :heavy_plus_sign: port over code from dwyl/abase #2 --- example_schema.js | 18 ++++ lib/config_validator.js | 24 +++++ lib/create_table_map.js | 32 ++++++ lib/helpers.js | 50 ++++++++++ lib/index.js | 49 ++++++++++ lib/parse.js | 34 +++++++ lib/sql_gen.js | 122 +++++++++++++++++++++++ lib/utils.js | 26 +++++ package.json | 8 +- test/config_validator.test.js | 73 ++++++++++++++ test/create_table_map.test.js | 85 ++++++++++++++++ test/db.test.js | 167 ++++++++++++++++++++++++++++++++ test/index.test.js | 55 +++++++++++ test/sql_gen.test.js | 177 ++++++++++++++++++++++++++++++++++ test/test_pg_client.js | 26 +++++ test/utils.test.js | 51 ++++++++++ 16 files changed, 995 insertions(+), 2 deletions(-) create mode 100644 example_schema.js create mode 100644 lib/config_validator.js create mode 100644 lib/create_table_map.js create mode 100644 lib/helpers.js create mode 100644 lib/index.js create mode 100644 lib/parse.js create mode 100644 lib/sql_gen.js create mode 100644 lib/utils.js create mode 100644 test/config_validator.test.js create mode 100644 test/create_table_map.test.js create mode 100644 test/db.test.js create mode 100644 test/index.test.js create mode 100644 test/sql_gen.test.js create mode 100644 test/test_pg_client.js create mode 100644 test/utils.test.js diff --git a/example_schema.js b/example_schema.js new file mode 100644 index 0000000..8b78c70 --- /dev/null +++ b/example_schema.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = { + table_name: 'user_data', // eslint-disable-line + fields: { + email: { + type: 'string', + email: true + }, + username: { + type: 'string', + min: 3, + max: 20, + unique: true + }, + dob: { type: 'date' } + } +}; diff --git a/lib/config_validator.js b/lib/config_validator.js new file mode 100644 index 0000000..d5c80f7 --- /dev/null +++ b/lib/config_validator.js @@ -0,0 +1,24 @@ +'use strict'; + +var Joi = require('joi'); + +var mapObj = require('./create_table_map.js').mapObj; + +// non empty, alphanumeric, no leading number, less than 64 +var dbNameRegEx = /^[A-Za-z_]\w{0,62}$/; +var fieldTypes = Object.keys(mapObj); + +var fieldSchema = Joi.object() + .keys({ type: Joi.any().valid(fieldTypes) }) + .unknown() +; +var configSchema = Joi.object().keys({ + table_name: Joi.string().regex(dbNameRegEx).required(), // eslint-disable-line + fields: Joi.object().pattern(dbNameRegEx, fieldSchema).required() // eslint-disable-line +}); + +module.exports = function (config) { + return Joi.assert(config, configSchema); +}; + +module.exports.dbNameRegEx = dbNameRegEx; diff --git a/lib/create_table_map.js b/lib/create_table_map.js new file mode 100644 index 0000000..55855f4 --- /dev/null +++ b/lib/create_table_map.js @@ -0,0 +1,32 @@ +'use strict'; + +var mapObj = { + number: function (opts) { + return opts.integer ? 'BIGINT' : 'DOUBLE PRECISION'; + }, + string: function (opts) { + var length = opts.max || 80; + + return 'VARCHAR(' + length + ')'; + }, + boolean: function () { + return 'BOOLEAN'; + }, + date: function (opts) { + return opts.timestamp ? 'TIMESTAMP' : 'DATE'; + } +}; + +function mapper (name, type, options) { + var opts = options || {}; + var constraints = ''; + + if (opts.unique) { + constraints += ' CONSTRAINT ' + name + '_unique UNIQUE'; + } + + return name + ' ' + mapObj[type](opts) + constraints; +} + +module.exports = mapper; +module.exports.mapObj = mapObj; diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 0000000..42da6e2 --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,50 @@ +'use strict'; + +var sqlGen = require('./sql_gen.js'); +var configValidator = require('./config_validator.js'); + +var methods = { + init: function (client, config, _, cb) { + var callback = (!cb && typeof _ === 'function') ? _ : cb; + + configValidator(config); + + return client.query(sqlGen.init(config), callback); + } +}; + +['select', 'update', 'delete', 'insert'].forEach(function (method) { + methods[method] = function (client, config, options, cb) { + var tableName = config.table_name; + var args = sqlGen[method](tableName, options).concat([cb]); + + return client.query.apply(client, args); + }; +}); + +methods.bindAll = function (pool, schema) { + return [ + 'select', 'update', 'delete', 'insert' + ].reduce(function (acc, method) { + acc[method] = function (options, cb) { + return pool.connect() + .then(function (client) { + return methods[method](client, schema, options) + .then(function (result) { + client.release(); + + return cb ? cb(null, result) : result; + }) + .catch(function (err) { + client.release(); + + return cb ? cb(err) : null; + }); + }); + }; + + return acc; + }, {}); +}; + +module.exports = methods; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..2ffe9a3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,49 @@ +/* + * Abase DB plugin + * + * Accepts path the schema defining the user model in the plugin options, + * or relies on the schema attached to the server settings object. + * + * Provides database helper functions to do schema-compatible CRUD operations. + * Attaches these methods to the request object at the pre-handler lifecycle + * point. + */ +'use strict'; + +var pg = require('pg'); +var db = require('./helpers.js'); +var parse = require('./parse.js'); + +exports.register = function (server, options, next) { + var schema = parse.schema(server.app.abase || {}, options.schemaPath); + + var connection = parse.dbConfig(options.dbConnection); + var pool = new pg.Pool(connection); + + pool.connect(function (dbConnErr, client, release) { + if (dbConnErr) { + return next(dbConnErr); + } + + return db.init(client, schema, function (dbErr) { + release(); + + if (dbErr) { + return next(dbErr); + } + + server.ext('onPreHandler', function (request, reply) { + request.abase = { db: db.bindAll(pool, schema) }; + reply.continue(); + }); + + server.decorate('server', 'endAbaseDb', function () { + pool.end(); + }); + + return next(); + }); + }); +}; + +exports.register.attributes = { name: 'abase-db' }; diff --git a/lib/parse.js b/lib/parse.js new file mode 100644 index 0000000..b249c9c --- /dev/null +++ b/lib/parse.js @@ -0,0 +1,34 @@ +'use strict'; + +var url = require('url'); + +exports.schema = function (schema, schemaPath) { + var sch; + + if (!schema || Object.keys(schema).length === 0) { + sch = require(schemaPath); // eslint-disable-line + } else { + sch = schema; + } + + return sch; +}; + + +exports.dbConfig = function (dbConnection) { + var parsed; + + if (typeof dbConnection === 'string') { + parsed = url.parse(dbConnection); + + return { + host: parsed.hostname, + port: parsed.port, + database: parsed.pathname.split('/')[1], + user: (parsed.auth || '').split(':')[0], + password: (parsed.auth || '').split(':')[1] + }; + } + + return dbConnection; +}; diff --git a/lib/sql_gen.js b/lib/sql_gen.js new file mode 100644 index 0000000..92c7ecb --- /dev/null +++ b/lib/sql_gen.js @@ -0,0 +1,122 @@ +'use strict'; + +var mapper = require('./create_table_map.js'); +var _ = require('./utils.js'); + + +function paramStr (columns, opts) { + var offset = (opts && opts.offset) || 0; + var assign = (opts && opts.assign) || false; + + return columns.map(function (k, i) { + var suff = '$' + (1 + i + (offset || 0)); + var pref = assign ? k + '=' : ''; + + return pref + suff; + }); +} + + +function processWhere (where, query, values) { + var keys = Object.keys(where); + var conds = paramStr(keys, { offset: values.length, assign: true }); + var vals = _.values(where, keys); + + return { + query: query.concat('WHERE').concat(conds.join(' AND ')), + values: values.concat(vals) + }; +} + + +exports.init = function init (config) { + var tableName = config.table_name; + var fields = config.fields; + + var columns = Object.keys(fields).map(function (key) { + var type = fields[key].type; + var opts = _.except(['type'], fields[key]); + + return mapper(key, type, opts); + }); + + return ['CREATE TABLE IF NOT EXISTS "' + tableName + '"'] + .concat('(' + columns.join(', ') + ')') + .join(' ') + .trim(); +}; + + +exports.select = function select (tableName, options) { + var columns = options.select || ['*']; + var values = []; + var query = ['SELECT'] + .concat(columns.join(', ')) + .concat('FROM') + .concat('"' + tableName + '"'); + var result; + + if (options.where) { + result = processWhere(options.where, query, values); + query = result.query; + values = result.values; + } + + query = query.join(' ').trim(); + + return [query, values]; +}; + + +exports.insert = function insert (tableName, options) { + var fields = options.fields || {}; + var columns = Object.keys(fields); + var values = _.values(fields, columns); + var params = paramStr(columns); + + var query = ['INSERT INTO "' + tableName + '"'] + .concat('(' + columns.join(', ') + ')') + .concat('VALUES') + .concat('(' + params.join(', ') + ')') + .join(' ') + .trim(); + + return [query, values]; +}; + + +exports.update = function update (tableName, options) { + var fields = options.fields || {}; + var columns = Object.keys(fields); + var conditions = paramStr(columns, { assign: true }); + var values = _.values(fields, columns); + + var query = ['UPDATE "' + tableName + '"'] + .concat('SET') + .concat(conditions.join(', ')); + var result; + + if (options.where) { + result = processWhere(options.where, query, values); + query = result.query; + values = result.values; + } + + query = query.join(' ').trim(); + + return [query, values]; +}; + + +exports.delete = function _delete (tableName, options) { + var query = ['DELETE FROM "' + tableName + '"']; + var values = []; + var result = processWhere(options.where, query, values); + + query = result.query; + values = result.values; + + query = query.join(' ').trim(); + + return [query, values]; +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..d432370 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,26 @@ +'use strict'; + +exports.values = function (obj, keys) { + return (keys || Object.keys(obj)) + .map(function (k) { return obj[k] }); +}; + + +function except (fields, obj) { + var o = {}; + + Object.keys(obj).forEach(function (k) { + if (fields.indexOf(k) === -1) { + o[k] = obj[k]; + } + }); + + return o; +} + + +exports.except = except; + +exports.shallowCopy = function (obj) { + return except([], obj); +}; diff --git a/package.json b/package.json index 22d5a1d..aee2ea8 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,14 @@ "goodparts": "^1.1.0", "istanbul": "^0.4.5", "pre-commit": "^1.1.3", - "tape": "^4.6.2" + "tape": "^4.6.2", + "hapi": "^15.1.1" }, "dependencies": { - "env2": "^2.1.1" + "env2": "^2.1.1", + "hoek": "^4.1.0", + "joi": "^9.0.4", + "pg": "^6.1.0" }, "scripts": { "test": "tape ./test/**/*.test.js", diff --git a/test/config_validator.test.js b/test/config_validator.test.js new file mode 100644 index 0000000..d2e1a48 --- /dev/null +++ b/test/config_validator.test.js @@ -0,0 +1,73 @@ +'use strict'; + +var test = require('tape'); + +var validate = require('../lib/config_validator.js'); +var dbNameRegEx = validate.dbNameRegEx; + +function validator (config) { + return function () { + validate(config); + }; +} + +test('config validator', function (t) { + t.throws( + validator({ fields: {} }), + 'error if no table_name property' + ); + t.throws( + validator({ table_name: 'test' }), // eslint-disable-line + 'error if no fields property' + ); + t.throws( + validator({ + table_name: '2test', // eslint-disable-line + fields: {} + }), + 'error if table name doesn\t pass db name regex' + ); + t.throws( + validator({ + table_name: 'test', // eslint-disable-line + fields: { '2field': { type: 'string' } } + }), + 'error if field name doesn\'t pass db name regex' + ); + t.doesNotThrow( + validator({ + table_name: 'test', // eslint-disable-line + fields: { email: { type: 'string', unknown: 'allowed' } } + }), + 'no error when extra options unknown' + ); + + t.end(); +}); + +test('dbNameRegEx', function (t) { + t.ok( + dbNameRegEx.exec('_a1pha_Numer1c'), + 'alpha numeric keys allowed only' + ); + t.notOk( + dbNameRegEx.exec(''), + 'alpha numeric keys allowed only' + ); + t.notOk( + dbNameRegEx.exec('no£way'), + 'no other characters allowed' + ); + t.notOk( + dbNameRegEx.exec('3Numer1c'), + 'must only start with a _ or letter' + ); + t.notOk( + dbNameRegEx.exec( + '_morethan63characters_morethan63characters_morethan63characters_' + ), + '63 character limit for field names' + ); + + t.end(); +}); diff --git a/test/create_table_map.test.js b/test/create_table_map.test.js new file mode 100644 index 0000000..46c4674 --- /dev/null +++ b/test/create_table_map.test.js @@ -0,0 +1,85 @@ +'use strict'; + +var test = require('tape'); + +var mapper = require('../lib/create_table_map.js'); + +var mapObj = mapper.mapObj; + +test('Boolean type', function (t) { + t.equal( + mapObj.boolean({}), + 'BOOLEAN', + 'boolean type: default' + ); + t.end(); +}); + +test('Date type', function (t) { + t.equal( + mapObj.date({}), + 'DATE', + 'date type: default' + ); + t.equal( + mapObj.date({ timestamp: true }), + 'TIMESTAMP', + 'date type: timestamp' + ); + t.end(); +}); + +test('Number type', function (t) { + t.equal( + mapObj.number({}), + 'DOUBLE PRECISION', + 'number type: default' + ); + t.equal( + mapObj.number({ integer: true }), + 'BIGINT', + 'number type: integer' + ); + t.end(); +}); + +test('String type', function (t) { + t.equal( + mapObj.string({}), + 'VARCHAR(80)', + 'string type: default' + ); + t.equal( + mapObj.string({ max: 12 }), + 'VARCHAR(12)', + 'string type: specifies length' + ); + t.end(); +}); + +test('Create Table Mapper Function', function (t) { + t.equal( + mapper('field', 'string', { max: 140 }), + 'field VARCHAR(140)', + 'name added to sql query and options passed through' + ); + t.end(); +}); + +test('Create Table Mapper Function w/ no options', function (t) { + t.equal( + mapper('field', 'string'), + 'field VARCHAR(80)', + 'name added to sql query and default options used' + ); + t.end(); +}); + +test('Create Table Mapper Function w/ unique option', function (t) { + t.equal( + mapper('email', 'string', { unique: true }), + 'email VARCHAR(80) CONSTRAINT email_unique UNIQUE', + 'constraint added to column' + ); + t.end(); +}); diff --git a/test/db.test.js b/test/db.test.js new file mode 100644 index 0000000..c5bb10f --- /dev/null +++ b/test/db.test.js @@ -0,0 +1,167 @@ +'use strict'; + +var test = require('tape'); + +var dbConn = require('./test_pg_client.js'); +var db = require('../lib/helpers.js'); +var schema = require('../example_schema.js'); + +var client = dbConn.client; +var pool = dbConn.pool; + +var testInsert = { + email: 'test@gmail.com', + dob: '2001-09-27', + username: 'test' +}; + +test('init test client', function (t) { + client.connect(function () { + client.query('DROP TABLE IF EXISTS ' + schema.table_name, t.end); + }); +}); + +test('db.init', function (t) { + t.throws( + function () { db.init(client, { rubbish: 'schema' }) }, + 'error thrown when given when using invalid schema' + ); + db.init(client, schema) + .then(function () { return client.query('SELECT * from user_data') }) + .then(function (res) { + t.ok( + res.fields + .map(function (field) { return field.name }) + .indexOf('dob') > -1 + , 'table created with a correct field' + ); + t.end(); + }) + ; +}); + + +test('db.insert & default select w custom where', function (t) { + db.insert(client, schema, { fields: testInsert }) + .then(function () { + return db.select(client, schema, { where: { dob: '2001-09-27' } }); + }) + .then(function (res) { + t.equal( + res.rows[0].email, + testInsert.email, + 'email correct' + ); + t.equal( + res.rows[0].username, + testInsert.username, + 'username correct' + ); + t.equal( + res.rows[0].dob.toLocaleDateString('GMT'), + new Date(testInsert.dob).toLocaleDateString('GMT'), + 'get same date back, though now a date object' + ); + t.end(); + }) + .catch(function (err) { + t.fail(err); + t.end(); + }) + ; +}); + +test('db.insert x 2 same username error', function (t) { + t.plan(1); + db.insert(client, schema, { fields: testInsert }) + .then(function () { + return db.insert(client, schema, { fields: testInsert }); + }) + .then(function () { + t.fails('shouldn\'t allow second insert if unique key given'); + }) + .catch(function () { + t.pass('shouldn\'t allow second insert if unique key given'); + }) + ; +}); + +test('db.update w where & custom select w default where', function (t) { + t.plan(1); + db.update(client, schema, { + fields: { username: 'bob' }, + where: { email: 'test@gmail.com' } + }).then(function () { + return db.select(client, schema, { select: ['email', 'username'] }); + }) + .then(function (res) { + t.deepEqual( + res.rows[0], + { + email: 'test@gmail.com', + username: 'bob' + }, + 'username updated' + ); + }) + .catch(t.fail); +}); + +test('db.delete w db.select', function (t) { + t.plan(1); + db.delete(client, schema, { where: { username: 'bob' } }) + .then(function () { return db.select(client, schema, {}) }) + .then(function (res) { t.equal(res.rows.length, 0, 'nothing left in db') }) + .catch(t.fail) + ; +}); + +test('db.bindAll returns obj w/ methods bound to pg.Pool', function (t) { + var dbBound = db.bindAll(pool, schema); + + t.equal(typeof dbBound.insert, 'function', '.insert method exists'); + t.equal(typeof dbBound.update, 'function', '.update method exists'); + t.equal(typeof dbBound.select, 'function', '.select method exists'); + t.equal(typeof dbBound.delete, 'function', '.delete method exists'); + t.end(); +}); + +test('db bound .insert adds to DB :: promise interface', function (t) { + var dbBound = db.bindAll(pool, schema); + + dbBound.insert({ fields: testInsert }) + .then(function () { + return dbBound.select({ where: { email: testInsert.email } }); + }) + .then(function (result) { + t.equal(result.rows[0].email, testInsert.email, 'Email matches'); + t.end(); + }) + .catch(t.fail); +}); + +test('db bound .delete removes line from DB :: cb interface', function (t) { + var dbBound = db.bindAll(pool, schema); + + dbBound.delete({ where: testInsert }, function (deleteErr) { + if (deleteErr) { + t.fail(deleteErr); + } + + dbBound.select({}, function (selectErr, result) { + if (selectErr) { + t.fail(selectErr); + } + + t.equal(result.rows.length, 0, 'Nothing left in DB'); + t.end(); + }); + }); +}); + +test('close test DB connections', function (t) { + pool.end(function () { + client.end(); + t.end(); + }); +}); diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..7ace97c --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,55 @@ +'use strict'; + +var test = require('tape'); +var Hapi = require('hapi'); +var path = require('path'); +var plugin = require('../lib/index.js'); + +var server = new Hapi.Server(); + +server.connection(); + +test('Can register DB plugin with `schemaPath` option', function (t) { + server.register({ + register: plugin, + options: { + schemaPath: path.resolve(__dirname, '..', 'example_schema.js'), + dbConnection: process.env.TEST_DATABASE_URL + } + }, function (err) { + if (err) { + t.fail(err); + } + + server.route({ + method: 'GET', + path: '/', + handler: function (request, reply) { + t.equal(typeof request.abase.db.insert, 'function', 'bound insert'); + t.equal(typeof request.abase.db.select, 'function', 'bound select'); + t.equal(typeof request.abase.db.update, 'function', 'bound update'); + t.equal(typeof request.abase.db.delete, 'function', 'bound delete'); + + request.abase.db.select({}) + .then(function (result) { + t.equal(result.rows.length, 0, 'nothing in the DB'); + reply(''); + }) + .catch(function () { + reply(''); + }); + } + }); + + server.inject({ method: 'GET', url: '/' }, function (response) { + t.equal(response.statusCode, 200, '200 OK Code'); + t.equal(response.payload, '', 'Empty (normal) response'); + t.end(); + }); + }); +}); + +test('Teardown', function (t) { + server.endAbaseDb(); + t.end(); +}); diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js new file mode 100644 index 0000000..9e6b94d --- /dev/null +++ b/test/sql_gen.test.js @@ -0,0 +1,177 @@ +'use strict'; + +var tape = require('tape'); + +var sqlGen = require('../lib/sql_gen.js'); +var schema = require('../example_schema.js'); + +tape('::init should throw on empty or invalid input', function (t) { + t.throws(function () { + sqlGen.init(); + }); + t.end(); +}); + +tape('::init - generate SQL to create a table if none exists', function (t) { + var query = sqlGen.init(schema); + + t.equal( + query, + 'CREATE TABLE IF NOT EXISTS "user_data" (' + + 'email VARCHAR(80), ' + + 'username VARCHAR(20) CONSTRAINT username_unique UNIQUE, ' + + 'dob DATE' + + ')', + 'Create table query generation from config object' + ); + t.end(); +}); + +tape('::select - generate SQL to select columns from a table', function (t) { + var query = sqlGen.select(schema.table_name, { select: ['email', 'dob'] }); + + t.equal( + query[0], + 'SELECT email, dob FROM "user_data"', + 'Generate parameterised query' + ); + t.deepEqual(query[1], [], 'Generate values for parameterised query'); + t.end(); +}); + +tape('::select - gen. SQL to select cols from table w/ where', function (t) { + var query = sqlGen.select(schema.table_name, { + select: ['email', 'dob'], + where: { foo: 'bar' } + }); + + t.equal( + query[0], + 'SELECT email, dob FROM "user_data" WHERE foo=$1', + 'Generate parameterised query' + ); + t.deepEqual(query[1], ['bar'], 'Generate values for parameterised query'); + t.end(); +}); + +tape('::insert - generate SQL to insert a column into a table', function (t) { + var query = sqlGen.insert( + schema.table_name, { fields: { email: 'me@poop.com' } } + ); + + t.equal( + query[0], + 'INSERT INTO "user_data" (email) VALUES ($1)', + 'Generate parameterised query' + ); + t.deepEqual( + query[1], + ['me@poop.com'], + 'Generate values for parameterised query' + ); + t.end(); +}); + +tape('::insert - generate SQL to insert blank col into table', function (t) { + var query = sqlGen.insert(schema.table_name, {}); + + t.equal( + query[0], + 'INSERT INTO "user_data" () VALUES ()', + 'Generate query for blank line' + ); + t.deepEqual( + query[1], + [], + 'Generate empty array' + ); + t.end(); +}); + +tape('::update - generate SQL to update a column in a table', function (t) { + var query = sqlGen.update( + schema.table_name, { fields: { email: 'me@poop.com' } } + ); + + t.equal( + query[0], + 'UPDATE "user_data" SET email=$1', + 'Generate parameterised query' + ); + t.deepEqual( + query[1], + ['me@poop.com'], + 'Generate values for parameterised query' + ); + t.end(); +}); + +tape('::update - generate SQL to update no fields of column', function (t) { + var query = sqlGen.update(schema.table_name, {}); + + t.equal( + query[0], + 'UPDATE "user_data" SET', + 'Generate query for blank line' + ); + t.deepEqual( + query[1], + [], + 'Generate empty array' + ); + t.end(); +}); + +tape('::update - gen. SQL to update a col in table w/ where', function (t) { + var query = sqlGen.update(schema.table_name, { + fields: { email: 'me@poop.com' }, + where: { foo: 'bar' } + }); + + t.equal( + query[0], + 'UPDATE "user_data" SET email=$1 WHERE foo=$2', + 'Generate parameterised query' + ); + t.deepEqual( + query[1], + ['me@poop.com', 'bar'], + 'Generate values for parameterised query' + ); + t.end(); +}); + +tape('::delete should generate SQL to delete a row from a table', function (t) { + var query = sqlGen.delete(schema.table_name, { where: { username: 'bob' } }); + + t.equal( + query[0], + 'DELETE FROM "user_data" WHERE username=$1', + 'Generate parameterised query' + ); + t.deepEqual( + query[1], + ['bob'], + 'Generate values for parameterised query' + ); + t.end(); +}); + +tape('::delete should gen SQL to delete row w/ multiple where', function (t) { + var query = sqlGen.delete( + schema.table_name, + { where: { username: 'bob', dob: '20/04/1988' } } + ); + + t.equal( + query[0], + 'DELETE FROM "user_data" WHERE username=$1 AND dob=$2', + 'Generate parameterised query' + ); + t.deepEqual( + query[1], + ['bob', '20/04/1988'], + 'Generate values for parameterised query' + ); + t.end(); +}); diff --git a/test/test_pg_client.js b/test/test_pg_client.js new file mode 100644 index 0000000..e0b4ab0 --- /dev/null +++ b/test/test_pg_client.js @@ -0,0 +1,26 @@ +'use strict'; + +var url = require('url'); +var pg = require('pg'); + +var parsed, connection; + +require('env2')('config.env'); + +if (!process.env.TEST_DATABASE_URL) { + throw new Error('TEST_DATABASE_URL must be defined'); +} + +parsed = url.parse(process.env.TEST_DATABASE_URL); +connection = { + host: parsed.hostname, + port: parsed.port, + database: parsed.pathname.split('/')[1], + user: (parsed.auth || '').split(':')[0], + password: (parsed.auth || '').split(':')[1] +}; + +module.exports = { + client: new pg.Client(connection), + pool: new pg.Pool(connection) +}; diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..2cf2614 --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,51 @@ +'use strict'; + +var test = require('tape'); +var _ = require('../lib/utils.js'); + +var o = { + a: 1, + b: 2 +}; + +test('::values w/ default keys value', function (t) { + var result = _.values(o); + + t.ok(result.indexOf(o.a) > -1, 'Key "a"\'s value found'); + t.ok(result.indexOf(o.b) > -1, 'Key "b"\'s value found'); + t.end(); +}); + +test('::values w/ chosen order', function (t) { + t.deepEqual( + _.values(o, ['b', 'a']), + [o.b, o.a], + '"b" given back first, "a" second' + ); + + t.end(); +}); + +test('::except', function (t) { + t.deepEqual( + _.except(['b'], o), + { a: 1 }, + 'Only "a" prop left' + ); + + t.end(); +}); + +test('::shallowCopy', function (t) { + var n = { + a: o, + b: 'c' + }; + var copy = _.shallowCopy(n); + + t.deepEqual(copy, n, 'deep equal'); + t.notEqual(copy, n, 'Not same object'); + t.equal(copy.a, o, 'Only shallowly copied'); + + t.end(); +}); From bb889a431ab2372d654c3560a2019bac07be9d14 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 25 Oct 2016 11:55:00 -0500 Subject: [PATCH 03/22] :heavy_plus_sign: :art: :white_check_mark: Clean up past code #2 --- lib/config_validator.js | 8 +- lib/db_handlers.js | 19 +++++ lib/helpers.js | 50 ----------- lib/index.js | 41 ++++----- lib/instantiate_db.js | 43 ++++++++++ lib/{parse.js => parse_options.js} | 18 ++-- lib/sql_gen.js | 16 ++-- test/{db.test.js => db_handlers.test.js} | 83 ++++++------------ test/index.test.js | 13 +-- test/istantiate_db.test.js | 102 +++++++++++++++++++++++ test/parse_options.test.js | 61 ++++++++++++++ test/sql_gen.test.js | 37 ++++---- 12 files changed, 314 insertions(+), 177 deletions(-) create mode 100644 lib/db_handlers.js delete mode 100644 lib/helpers.js create mode 100644 lib/instantiate_db.js rename lib/{parse.js => parse_options.js} (60%) rename test/{db.test.js => db_handlers.test.js} (55%) create mode 100644 test/istantiate_db.test.js create mode 100644 test/parse_options.test.js diff --git a/lib/config_validator.js b/lib/config_validator.js index d5c80f7..0c528f4 100644 --- a/lib/config_validator.js +++ b/lib/config_validator.js @@ -13,8 +13,12 @@ var fieldSchema = Joi.object() .unknown() ; var configSchema = Joi.object().keys({ - table_name: Joi.string().regex(dbNameRegEx).required(), // eslint-disable-line - fields: Joi.object().pattern(dbNameRegEx, fieldSchema).required() // eslint-disable-line + table_name: Joi.string() + .regex(dbNameRegEx) + .required(), + fields: Joi.object() + .pattern(dbNameRegEx, fieldSchema) + .required() }); module.exports = function (config) { diff --git a/lib/db_handlers.js b/lib/db_handlers.js new file mode 100644 index 0000000..43150ae --- /dev/null +++ b/lib/db_handlers.js @@ -0,0 +1,19 @@ +'use strict'; + +var sqlGen = require('./sql_gen.js'); + +var methods = { + init: function (client, config, _, cb) { + return client.query(sqlGen.init(config), cb); + } +}; + +['select', 'update', 'delete', 'insert'].forEach(function (method) { + methods[method] = function (client, _, options, cb) { + var args = sqlGen[method](options).concat([cb]); + + return client.query.apply(client, args); + }; +}); + +module.exports = methods; diff --git a/lib/helpers.js b/lib/helpers.js deleted file mode 100644 index 42da6e2..0000000 --- a/lib/helpers.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -var sqlGen = require('./sql_gen.js'); -var configValidator = require('./config_validator.js'); - -var methods = { - init: function (client, config, _, cb) { - var callback = (!cb && typeof _ === 'function') ? _ : cb; - - configValidator(config); - - return client.query(sqlGen.init(config), callback); - } -}; - -['select', 'update', 'delete', 'insert'].forEach(function (method) { - methods[method] = function (client, config, options, cb) { - var tableName = config.table_name; - var args = sqlGen[method](tableName, options).concat([cb]); - - return client.query.apply(client, args); - }; -}); - -methods.bindAll = function (pool, schema) { - return [ - 'select', 'update', 'delete', 'insert' - ].reduce(function (acc, method) { - acc[method] = function (options, cb) { - return pool.connect() - .then(function (client) { - return methods[method](client, schema, options) - .then(function (result) { - client.release(); - - return cb ? cb(null, result) : result; - }) - .catch(function (err) { - client.release(); - - return cb ? cb(err) : null; - }); - }); - }; - - return acc; - }, {}); -}; - -module.exports = methods; diff --git a/lib/index.js b/lib/index.js index 2ffe9a3..d8ffbac 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,38 +11,29 @@ 'use strict'; var pg = require('pg'); -var db = require('./helpers.js'); -var parse = require('./parse.js'); -exports.register = function (server, options, next) { - var schema = parse.schema(server.app.abase || {}, options.schemaPath); +var parseOptions = require('./parse_options.js'); +var instantiateDb = require('./instantiate_db.js'); +var configValidator = require('./config_validator.js'); - var connection = parse.dbConfig(options.dbConnection); +exports.register = function (server, options, next) { + var schema = parseOptions.schema(options, server); + var connection = parseOptions.dbConfig(options.dbConnection); var pool = new pg.Pool(connection); - pool.connect(function (dbConnErr, client, release) { - if (dbConnErr) { - return next(dbConnErr); - } - - return db.init(client, schema, function (dbErr) { - release(); - - if (dbErr) { - return next(dbErr); - } + configValidator(schema); - server.ext('onPreHandler', function (request, reply) { - request.abase = { db: db.bindAll(pool, schema) }; - reply.continue(); - }); - - server.decorate('server', 'endAbaseDb', function () { - pool.end(); - }); + return instantiateDb(pool, schema, function (dbErr, db) { + server.ext('onPreHandler', function (request, reply) { + request.abase = { db: db }; + reply.continue(); + }); - return next(); + server.decorate('server', 'endAbaseDb', function (cb) { + pool.end(cb); }); + + return next(dbErr); }); }; diff --git a/lib/instantiate_db.js b/lib/instantiate_db.js new file mode 100644 index 0000000..9244197 --- /dev/null +++ b/lib/instantiate_db.js @@ -0,0 +1,43 @@ +'use strict'; + +var handlers = require('./db_handlers.js'); + +var exposedHandlers = ['select', 'update', 'delete', 'insert']; + +function bindPoolClient (schema, handler, pool) { + return function (options, cb) { + return pool.connect() + .then(function (client) { + return handlers[handler](client, schema, options) + .then(function (result) { + client.release(); + + return cb ? cb(null, result) : result; + }) + .catch(function (err) { + client.release(); + + return cb ? cb(err) : null; + }) + ; + }) + .catch(function (err) { + return cb ? cb(err) : null; + }) + ; + }; +} + +function bindHandlers (pool, schema) { + return exposedHandlers.reduce(function (acc, handler) { + acc[handler] = bindPoolClient(schema, handler, pool); + + return acc; + }, {}); +} + +module.exports = function (pool, schema, callback) { + return bindPoolClient(schema, 'init', pool)(null, function (err) { + return callback(err, bindHandlers(pool, schema)); + }); +}; diff --git a/lib/parse.js b/lib/parse_options.js similarity index 60% rename from lib/parse.js rename to lib/parse_options.js index b249c9c..1b71f6e 100644 --- a/lib/parse.js +++ b/lib/parse_options.js @@ -2,21 +2,17 @@ var url = require('url'); -exports.schema = function (schema, schemaPath) { - var sch; - - if (!schema || Object.keys(schema).length === 0) { - sch = require(schemaPath); // eslint-disable-line - } else { - sch = schema; - } - - return sch; +exports.schema = function (options, server) { + return server.app.abase + || options.schema + || require(options.schemaPath) // eslint-disable-line + ; }; -exports.dbConfig = function (dbConnection) { +exports.dbConfig = function (options) { var parsed; + var dbConnection = options.dbConnection; if (typeof dbConnection === 'string') { parsed = url.parse(dbConnection); diff --git a/lib/sql_gen.js b/lib/sql_gen.js index 92c7ecb..7e78f22 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -47,13 +47,13 @@ exports.init = function init (config) { }; -exports.select = function select (tableName, options) { +exports.select = function select (options) { var columns = options.select || ['*']; var values = []; var query = ['SELECT'] .concat(columns.join(', ')) .concat('FROM') - .concat('"' + tableName + '"'); + .concat('"' + options.tableName + '"'); var result; if (options.where) { @@ -68,13 +68,13 @@ exports.select = function select (tableName, options) { }; -exports.insert = function insert (tableName, options) { +exports.insert = function insert (options) { var fields = options.fields || {}; var columns = Object.keys(fields); var values = _.values(fields, columns); var params = paramStr(columns); - var query = ['INSERT INTO "' + tableName + '"'] + var query = ['INSERT INTO "' + options.tableName + '"'] .concat('(' + columns.join(', ') + ')') .concat('VALUES') .concat('(' + params.join(', ') + ')') @@ -85,13 +85,13 @@ exports.insert = function insert (tableName, options) { }; -exports.update = function update (tableName, options) { +exports.update = function update (options) { var fields = options.fields || {}; var columns = Object.keys(fields); var conditions = paramStr(columns, { assign: true }); var values = _.values(fields, columns); - var query = ['UPDATE "' + tableName + '"'] + var query = ['UPDATE "' + options.tableName + '"'] .concat('SET') .concat(conditions.join(', ')); var result; @@ -108,8 +108,8 @@ exports.update = function update (tableName, options) { }; -exports.delete = function _delete (tableName, options) { - var query = ['DELETE FROM "' + tableName + '"']; +exports.delete = function _delete (options) { + var query = ['DELETE FROM "' + options.tableName + '"']; var values = []; var result = processWhere(options.where, query, values); diff --git a/test/db.test.js b/test/db_handlers.test.js similarity index 55% rename from test/db.test.js rename to test/db_handlers.test.js index c5bb10f..2044994 100644 --- a/test/db.test.js +++ b/test/db_handlers.test.js @@ -3,17 +3,18 @@ var test = require('tape'); var dbConn = require('./test_pg_client.js'); -var db = require('../lib/helpers.js'); +var db = require('../lib/db_handlers.js'); var schema = require('../example_schema.js'); var client = dbConn.client; -var pool = dbConn.pool; + var testInsert = { email: 'test@gmail.com', dob: '2001-09-27', username: 'test' }; +var testTab = schema.table_name; test('init test client', function (t) { client.connect(function () { @@ -42,9 +43,12 @@ test('db.init', function (t) { test('db.insert & default select w custom where', function (t) { - db.insert(client, schema, { fields: testInsert }) + db.insert(client, schema, { fields: testInsert, tableName: testTab }) .then(function () { - return db.select(client, schema, { where: { dob: '2001-09-27' } }); + return db.select(client, schema, { + where: { dob: '2001-09-27' }, + tableName: testTab + }); }) .then(function (res) { t.equal( @@ -58,8 +62,8 @@ test('db.insert & default select w custom where', function (t) { 'username correct' ); t.equal( - res.rows[0].dob.toLocaleDateString('GMT'), - new Date(testInsert.dob).toLocaleDateString('GMT'), + res.rows[0].dob.getFullYear(), + new Date(testInsert.dob).getFullYear(), 'get same date back, though now a date object' ); t.end(); @@ -73,9 +77,12 @@ test('db.insert & default select w custom where', function (t) { test('db.insert x 2 same username error', function (t) { t.plan(1); - db.insert(client, schema, { fields: testInsert }) + db.insert(client, schema, { fields: testInsert, tableName: testTab }) .then(function () { - return db.insert(client, schema, { fields: testInsert }); + return db.insert(client, schema, { + fields: testInsert, + tableName: testTab + }); }) .then(function () { t.fails('shouldn\'t allow second insert if unique key given'); @@ -89,10 +96,14 @@ test('db.insert x 2 same username error', function (t) { test('db.update w where & custom select w default where', function (t) { t.plan(1); db.update(client, schema, { + tableName: testTab, fields: { username: 'bob' }, where: { email: 'test@gmail.com' } }).then(function () { - return db.select(client, schema, { select: ['email', 'username'] }); + return db.select(client, schema, { + tableName: testTab, + select: ['email', 'username'] + }); }) .then(function (res) { t.deepEqual( @@ -109,59 +120,15 @@ test('db.update w where & custom select w default where', function (t) { test('db.delete w db.select', function (t) { t.plan(1); - db.delete(client, schema, { where: { username: 'bob' } }) - .then(function () { return db.select(client, schema, {}) }) + db.delete(client, schema, { tableName: testTab, where: { username: 'bob' } }) + .then(function () { + return db.select(client, schema, { tableName: testTab }); + }) .then(function (res) { t.equal(res.rows.length, 0, 'nothing left in db') }) .catch(t.fail) ; }); -test('db.bindAll returns obj w/ methods bound to pg.Pool', function (t) { - var dbBound = db.bindAll(pool, schema); - - t.equal(typeof dbBound.insert, 'function', '.insert method exists'); - t.equal(typeof dbBound.update, 'function', '.update method exists'); - t.equal(typeof dbBound.select, 'function', '.select method exists'); - t.equal(typeof dbBound.delete, 'function', '.delete method exists'); - t.end(); -}); - -test('db bound .insert adds to DB :: promise interface', function (t) { - var dbBound = db.bindAll(pool, schema); - - dbBound.insert({ fields: testInsert }) - .then(function () { - return dbBound.select({ where: { email: testInsert.email } }); - }) - .then(function (result) { - t.equal(result.rows[0].email, testInsert.email, 'Email matches'); - t.end(); - }) - .catch(t.fail); -}); - -test('db bound .delete removes line from DB :: cb interface', function (t) { - var dbBound = db.bindAll(pool, schema); - - dbBound.delete({ where: testInsert }, function (deleteErr) { - if (deleteErr) { - t.fail(deleteErr); - } - - dbBound.select({}, function (selectErr, result) { - if (selectErr) { - t.fail(selectErr); - } - - t.equal(result.rows.length, 0, 'Nothing left in DB'); - t.end(); - }); - }); -}); - test('close test DB connections', function (t) { - pool.end(function () { - client.end(); - t.end(); - }); + client.end(t.end); }); diff --git a/test/index.test.js b/test/index.test.js index 7ace97c..6e46992 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -5,11 +5,11 @@ var Hapi = require('hapi'); var path = require('path'); var plugin = require('../lib/index.js'); -var server = new Hapi.Server(); - -server.connection(); test('Can register DB plugin with `schemaPath` option', function (t) { + var server = new Hapi.Server(); + + server.connection(); server.register({ register: plugin, options: { @@ -44,12 +44,7 @@ test('Can register DB plugin with `schemaPath` option', function (t) { server.inject({ method: 'GET', url: '/' }, function (response) { t.equal(response.statusCode, 200, '200 OK Code'); t.equal(response.payload, '', 'Empty (normal) response'); - t.end(); + server.endAbaseDb(t.end); }); }); }); - -test('Teardown', function (t) { - server.endAbaseDb(); - t.end(); -}); diff --git a/test/istantiate_db.test.js b/test/istantiate_db.test.js new file mode 100644 index 0000000..603b9b1 --- /dev/null +++ b/test/istantiate_db.test.js @@ -0,0 +1,102 @@ +'use strict'; + +var test = require('tape'); + +var dbConn = require('./test_pg_client.js'); +var instantiateDb = require('../lib/instantiate_db.js'); +var schema = require('../example_schema.js'); + +var pool = dbConn.pool; + +var testInsert = { + email: 'test@gmail.com', + dob: '2001-09-27', + username: 'test' +}; +var testTab = schema.table_name; + +test('instantiateDb gives obj w/ methods bound to pg.Pool to cb', function (t) { + instantiateDb(pool, schema, function (err, db) { + if (err) { + t.fail(err, 'should work ok'); + } + t.equal(typeof db.insert, 'function', '.insert method exists'); + t.equal(typeof db.update, 'function', '.update method exists'); + t.equal(typeof db.select, 'function', '.select method exists'); + t.equal(typeof db.delete, 'function', '.delete method exists'); + t.end(); + }); +}); + +test('db bound .insert adds to DB :: promise interface', function (t) { + instantiateDb(pool, schema, function (_, db) { + db.insert({ fields: testInsert, tableName: testTab }) + .then(function () { + return db.select({ + tableName: testTab, + where: { email: testInsert.email } + }); + }) + .then(function (result) { + t.equal(result.rows[0].email, testInsert.email, 'Email matches'); + t.end(); + }) + .catch(t.fail) + ; + }); +}); + +test('db bound .delete removes line from DB :: cb interface', function (t) { + instantiateDb(pool, schema, function (_, db) { + db.delete({ tableName: testTab, where: testInsert }, function (deleteErr) { + if (deleteErr) { + t.fail(deleteErr); + } + + db.select({ tableName: testTab }, function (selectErr, result) { + if (selectErr) { + t.fail(selectErr); + } + + t.equal(result.rows.length, 0, 'Nothing left in DB'); + t.end(); + }); + }); + }); +}); + +test('invalid args error for handler given to cb', function (t) { + instantiateDb(pool, schema, function (_, db) { + db.delete( + { tableName: testTab, where: 'Should not be a string' }, + function (handlerError) { + t.ok(handlerError, 'callback given error from handler being abused'); + + t.end(); + } + ); + }); +}); + +test('pool error', function (t) { + t.plan(2); + + instantiateDb(pool, schema, function (_, db) { + pool.end(function () { + db.delete( + { tableName: testTab, where: testInsert }, + function (poolError) { + t.ok(poolError, 'callback given error from failed pool connection'); + } + ); + db.delete({ tableName: testTab, where: testInsert }).then(function (res) { + t.notOk(res, 'no returned result if have caught error and no cb'); + }); + }); + }); +}); + +// keep at bottom +test('close test DB connections', function (t) { + pool.end(t.end); +}); diff --git a/test/parse_options.test.js b/test/parse_options.test.js new file mode 100644 index 0000000..9c5af54 --- /dev/null +++ b/test/parse_options.test.js @@ -0,0 +1,61 @@ +'use strict'; + +var test = require('tape'); +var path = require('path'); + +var parseOptions = require('../lib/parse_options.js'); + + +test('parseOptions.schema', function (t) { + t.deepEqual( + parseOptions.schema( + { + schema: { object: 'schema' }, + schemaPath: path.resolve(__dirname, '..', 'example_schema.js') + }, + { app: { abase: { server: 'schema' } } } + ), + { server: 'schema' }, + 'server trumps schema object' + ); + t.deepEqual( + parseOptions.schema( + { + schema: { table: 'schema' }, + schemaPath: path.resolve(__dirname, '..', 'example_schema.js') + }, + { app: {} } + ), + { table: 'schema' }, + 'schema trumps schemaPath' + ); + t.deepEqual( + parseOptions.schema( + { schemaPath: path.resolve(__dirname, '..', 'example_schema.js') }, + { app: {} } + ), + require('../example_schema.js'), // eslint-disable-line + 'schema trumps schemaPath' + ); + t.end(); +}); + +test('parseOptions.dbConfig', function (t) { + t.deepEqual( + parseOptions.dbConfig({ dbConnection: { parsed: 'object' } }), + { parsed: 'object' }, + 'does nothing if config already complete' + ); + t.deepEqual( + parseOptions.dbConfig({ dbConnection: 'psql://localhost:5432/testdb' }), + { + database: 'testdb', + host: 'localhost', + password: undefined, // eslint-disable-line + port: '5432', + user: '' + }, + 'parses db url and handles no user and pass' + ); + t.end(); +}); diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js index 9e6b94d..1c65784 100644 --- a/test/sql_gen.test.js +++ b/test/sql_gen.test.js @@ -28,7 +28,10 @@ tape('::init - generate SQL to create a table if none exists', function (t) { }); tape('::select - generate SQL to select columns from a table', function (t) { - var query = sqlGen.select(schema.table_name, { select: ['email', 'dob'] }); + var query = sqlGen.select({ + tableName: schema.table_name, + select: ['email', 'dob'] + }); t.equal( query[0], @@ -40,7 +43,8 @@ tape('::select - generate SQL to select columns from a table', function (t) { }); tape('::select - gen. SQL to select cols from table w/ where', function (t) { - var query = sqlGen.select(schema.table_name, { + var query = sqlGen.select({ + tableName: schema.table_name, select: ['email', 'dob'], where: { foo: 'bar' } }); @@ -56,7 +60,7 @@ tape('::select - gen. SQL to select cols from table w/ where', function (t) { tape('::insert - generate SQL to insert a column into a table', function (t) { var query = sqlGen.insert( - schema.table_name, { fields: { email: 'me@poop.com' } } + { tableName: schema.table_name, fields: { email: 'me@poop.com' } } ); t.equal( @@ -73,7 +77,7 @@ tape('::insert - generate SQL to insert a column into a table', function (t) { }); tape('::insert - generate SQL to insert blank col into table', function (t) { - var query = sqlGen.insert(schema.table_name, {}); + var query = sqlGen.insert({ tableName: schema.table_name }); t.equal( query[0], @@ -89,9 +93,10 @@ tape('::insert - generate SQL to insert blank col into table', function (t) { }); tape('::update - generate SQL to update a column in a table', function (t) { - var query = sqlGen.update( - schema.table_name, { fields: { email: 'me@poop.com' } } - ); + var query = sqlGen.update({ + tableName: schema.table_name, + fields: { email: 'me@poop.com' } + }); t.equal( query[0], @@ -107,7 +112,7 @@ tape('::update - generate SQL to update a column in a table', function (t) { }); tape('::update - generate SQL to update no fields of column', function (t) { - var query = sqlGen.update(schema.table_name, {}); + var query = sqlGen.update({ tableName: schema.table_name }); t.equal( query[0], @@ -123,7 +128,8 @@ tape('::update - generate SQL to update no fields of column', function (t) { }); tape('::update - gen. SQL to update a col in table w/ where', function (t) { - var query = sqlGen.update(schema.table_name, { + var query = sqlGen.update({ + tableName: schema.table_name, fields: { email: 'me@poop.com' }, where: { foo: 'bar' } }); @@ -142,7 +148,10 @@ tape('::update - gen. SQL to update a col in table w/ where', function (t) { }); tape('::delete should generate SQL to delete a row from a table', function (t) { - var query = sqlGen.delete(schema.table_name, { where: { username: 'bob' } }); + var query = sqlGen.delete({ + tableName: schema.table_name, + where: { username: 'bob' } + }); t.equal( query[0], @@ -158,10 +167,10 @@ tape('::delete should generate SQL to delete a row from a table', function (t) { }); tape('::delete should gen SQL to delete row w/ multiple where', function (t) { - var query = sqlGen.delete( - schema.table_name, - { where: { username: 'bob', dob: '20/04/1988' } } - ); + var query = sqlGen.delete({ + tableName: schema.table_name, + where: { username: 'bob', dob: '20/04/1988' } + }); t.equal( query[0], From 1eeacd104accbca42d5854acf6ecd202c954af3c Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Wed, 26 Oct 2016 09:44:02 -0500 Subject: [PATCH 04/22] :heavy_plus_sign: Except single schema or array #5 --- lib/config_validator.js | 4 +++- lib/db_handlers.js | 19 +++++++++++++++--- package.json | 2 +- test/config_validator.test.js | 15 ++++++++++++++ test/db_handlers.test.js | 37 ++++++++++++++++++++++++++++------- 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/lib/config_validator.js b/lib/config_validator.js index 0c528f4..5307758 100644 --- a/lib/config_validator.js +++ b/lib/config_validator.js @@ -12,7 +12,7 @@ var fieldSchema = Joi.object() .keys({ type: Joi.any().valid(fieldTypes) }) .unknown() ; -var configSchema = Joi.object().keys({ +var tableSchema = Joi.object().keys({ table_name: Joi.string() .regex(dbNameRegEx) .required(), @@ -21,6 +21,8 @@ var configSchema = Joi.object().keys({ .required() }); +var configSchema = [tableSchema, Joi.array().items(tableSchema)]; + module.exports = function (config) { return Joi.assert(config, configSchema); }; diff --git a/lib/db_handlers.js b/lib/db_handlers.js index 43150ae..be9dedf 100644 --- a/lib/db_handlers.js +++ b/lib/db_handlers.js @@ -2,10 +2,23 @@ var sqlGen = require('./sql_gen.js'); -var methods = { - init: function (client, config, _, cb) { - return client.query(sqlGen.init(config), cb); +var methods = {}; + +function multipleQuery (client, queries, cb) { + function nextQuery () { + var last = queries.length === 1; + + return client.query(queries.pop(), !last ? nextQuery : cb); } + + return nextQuery(); +} + +methods.init = function (client, config, _, cb) { + var tables = [].concat(config); + var queries = tables.map(sqlGen.init); + + return multipleQuery(client, queries, cb); }; ['select', 'update', 'delete', 'insert'].forEach(function (method) { diff --git a/package.json b/package.json index aee2ea8..97971b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "1.0.0", + "version": "0.0.1", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/index.js", "devDependencies": { diff --git a/test/config_validator.test.js b/test/config_validator.test.js index d2e1a48..bbb00f9 100644 --- a/test/config_validator.test.js +++ b/test/config_validator.test.js @@ -45,6 +45,21 @@ test('config validator', function (t) { t.end(); }); +test('config validator, multiple tables', function (t) { + t.doesNotThrow( + validator([{ + table_name: 'test', // eslint-disable-line + fields: { email: { type: 'string' } } + }, { + table_name: 'test_2', // eslint-disable-line + fields: { email: { type: 'string' } } + }]), + 'handles multiple tables' + ); + + t.end(); +}); + test('dbNameRegEx', function (t) { t.ok( dbNameRegEx.exec('_a1pha_Numer1c'), diff --git a/test/db_handlers.test.js b/test/db_handlers.test.js index 2044994..180ca68 100644 --- a/test/db_handlers.test.js +++ b/test/db_handlers.test.js @@ -6,8 +6,13 @@ var dbConn = require('./test_pg_client.js'); var db = require('../lib/db_handlers.js'); var schema = require('../example_schema.js'); -var client = dbConn.client; - +var multipleSchema = [{ + table_name: 'table_1', // eslint-disable-line + fields: { field: { type: 'string', email: true } } +}, { + table_name: 'table_2', // eslint-disable-line + fields: { field: { type: 'string', email: true } } +}]; var testInsert = { email: 'test@gmail.com', @@ -16,17 +21,17 @@ var testInsert = { }; var testTab = schema.table_name; +var client = dbConn.client; + test('init test client', function (t) { client.connect(function () { - client.query('DROP TABLE IF EXISTS ' + schema.table_name, t.end); + client.query('DROP TABLE IF EXISTS ' + schema.table_name); + client.query('DROP TABLE IF EXISTS table_1'); + client.query('DROP TABLE IF EXISTS table_2', t.end); }); }); test('db.init', function (t) { - t.throws( - function () { db.init(client, { rubbish: 'schema' }) }, - 'error thrown when given when using invalid schema' - ); db.init(client, schema) .then(function () { return client.query('SELECT * from user_data') }) .then(function (res) { @@ -41,6 +46,24 @@ test('db.init', function (t) { ; }); +test('db.init multiple tables', function (t) { + function checkFieldExist (res) { + t.ok( + res.fields + .map(function (field) { return field.name }) + .indexOf('field') > -1 + , 'table created with a correct field' + ); + } + db.init(client, multipleSchema) + .then(function () { return client.query('SELECT * from table_1') }) + .then(checkFieldExist) + .then(function () { return client.query('SELECT * from table_2') }) + .then(checkFieldExist) + .then(t.end) + ; +}); + test('db.insert & default select w custom where', function (t) { db.insert(client, schema, { fields: testInsert, tableName: testTab }) From 15ad9cc202fc5ce82d90a27ae43cd5bfb97b477d Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Wed, 26 Oct 2016 09:56:03 -0500 Subject: [PATCH 05/22] :heavy+plus_sign: Expose db handlers from module #7 --- lib/index.js | 3 +++ package.json | 2 +- test/index.test.js | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index d8ffbac..0536142 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,6 +15,7 @@ var pg = require('pg'); var parseOptions = require('./parse_options.js'); var instantiateDb = require('./instantiate_db.js'); var configValidator = require('./config_validator.js'); +var handlers = require('./db_handlers.js'); exports.register = function (server, options, next) { var schema = parseOptions.schema(options, server); @@ -38,3 +39,5 @@ exports.register = function (server, options, next) { }; exports.register.attributes = { name: 'abase-db' }; + +exports.handlers = handlers; diff --git a/package.json b/package.json index 97971b4..3de1491 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "0.0.1", + "version": "0.0.2", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/index.js", "devDependencies": { diff --git a/test/index.test.js b/test/index.test.js index 6e46992..a403c08 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -48,3 +48,16 @@ test('Can register DB plugin with `schemaPath` option', function (t) { }); }); }); + +test('db handlers exposed', function (t) { + var handlers = Object.keys(plugin.handlers); + var wanted = ['insert', 'select', 'delete', 'update', 'init']; + + t.ok( + wanted.reduce(function (truth, handler) { + return truth && handlers.indexOf(handler) > -1; + }, true), + 'all handlers found: ' + wanted.join(', ') + ); + t.end(); +}); From e9b9d558968686181ed25cbbbc12c6cbdfd0d843 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Thu, 27 Oct 2016 00:13:23 -0500 Subject: [PATCH 06/22] :heavy_plus_sign: FLush handler #9 Also renames table_name to tableName for consistency between options and to not encourage snake case --- example_schema.js | 2 +- lib/config_validator.js | 2 +- lib/db_handlers.js | 7 +++++++ lib/sql_gen.js | 6 +++++- package.json | 2 +- test/config_validator.test.js | 14 +++++++------- test/db_handlers.test.js | 33 +++++++++++++++++++++++++++++---- test/index.test.js | 2 +- test/istantiate_db.test.js | 2 +- test/sql_gen.test.js | 29 ++++++++++++++++++++--------- 10 files changed, 73 insertions(+), 26 deletions(-) diff --git a/example_schema.js b/example_schema.js index 8b78c70..915c5b7 100644 --- a/example_schema.js +++ b/example_schema.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - table_name: 'user_data', // eslint-disable-line + tableName: 'user_data', fields: { email: { type: 'string', diff --git a/lib/config_validator.js b/lib/config_validator.js index 5307758..524ce84 100644 --- a/lib/config_validator.js +++ b/lib/config_validator.js @@ -13,7 +13,7 @@ var fieldSchema = Joi.object() .unknown() ; var tableSchema = Joi.object().keys({ - table_name: Joi.string() + tableName: Joi.string() .regex(dbNameRegEx) .required(), fields: Joi.object() diff --git a/lib/db_handlers.js b/lib/db_handlers.js index be9dedf..9918b05 100644 --- a/lib/db_handlers.js +++ b/lib/db_handlers.js @@ -21,6 +21,13 @@ methods.init = function (client, config, _, cb) { return multipleQuery(client, queries, cb); }; +methods.flush = function (client, config, options, cb) { + var tables = [].concat(options || config); + var queries = tables.map(sqlGen.dropTable); + + return multipleQuery(client, queries, cb); +}; + ['select', 'update', 'delete', 'insert'].forEach(function (method) { methods[method] = function (client, _, options, cb) { var args = sqlGen[method](options).concat([cb]); diff --git a/lib/sql_gen.js b/lib/sql_gen.js index 7e78f22..1c5597b 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -30,7 +30,7 @@ function processWhere (where, query, values) { exports.init = function init (config) { - var tableName = config.table_name; + var tableName = config.tableName; var fields = config.fields; var columns = Object.keys(fields).map(function (key) { @@ -120,3 +120,7 @@ exports.delete = function _delete (options) { return [query, values]; }; + +exports.dropTable = function dropTable (options) { + return 'DROP TABLE "' + options.tableName + '";'; +}; diff --git a/package.json b/package.json index 3de1491..4e41806 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "0.0.2", + "version": "0.1.0", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/index.js", "devDependencies": { diff --git a/test/config_validator.test.js b/test/config_validator.test.js index bbb00f9..616b391 100644 --- a/test/config_validator.test.js +++ b/test/config_validator.test.js @@ -14,29 +14,29 @@ function validator (config) { test('config validator', function (t) { t.throws( validator({ fields: {} }), - 'error if no table_name property' + 'error if no tableName property' ); t.throws( - validator({ table_name: 'test' }), // eslint-disable-line + validator({ tableName: 'test' }), // eslint-disable-line 'error if no fields property' ); t.throws( validator({ - table_name: '2test', // eslint-disable-line + tableName: '2test', fields: {} }), 'error if table name doesn\t pass db name regex' ); t.throws( validator({ - table_name: 'test', // eslint-disable-line + tableName: 'test', fields: { '2field': { type: 'string' } } }), 'error if field name doesn\'t pass db name regex' ); t.doesNotThrow( validator({ - table_name: 'test', // eslint-disable-line + tableName: 'test', fields: { email: { type: 'string', unknown: 'allowed' } } }), 'no error when extra options unknown' @@ -48,10 +48,10 @@ test('config validator', function (t) { test('config validator, multiple tables', function (t) { t.doesNotThrow( validator([{ - table_name: 'test', // eslint-disable-line + tableName: 'test', fields: { email: { type: 'string' } } }, { - table_name: 'test_2', // eslint-disable-line + tableName: 'test_2', fields: { email: { type: 'string' } } }]), 'handles multiple tables' diff --git a/test/db_handlers.test.js b/test/db_handlers.test.js index 180ca68..c29ca23 100644 --- a/test/db_handlers.test.js +++ b/test/db_handlers.test.js @@ -7,10 +7,10 @@ var db = require('../lib/db_handlers.js'); var schema = require('../example_schema.js'); var multipleSchema = [{ - table_name: 'table_1', // eslint-disable-line + tableName: 'table_1', // eslint-disable-line fields: { field: { type: 'string', email: true } } }, { - table_name: 'table_2', // eslint-disable-line + tableName: 'table_2', // eslint-disable-line fields: { field: { type: 'string', email: true } } }]; @@ -19,13 +19,13 @@ var testInsert = { dob: '2001-09-27', username: 'test' }; -var testTab = schema.table_name; +var testTab = schema.tableName; var client = dbConn.client; test('init test client', function (t) { client.connect(function () { - client.query('DROP TABLE IF EXISTS ' + schema.table_name); + client.query('DROP TABLE IF EXISTS ' + schema.tableName); client.query('DROP TABLE IF EXISTS table_1'); client.query('DROP TABLE IF EXISTS table_2', t.end); }); @@ -152,6 +152,31 @@ test('db.delete w db.select', function (t) { ; }); +test('db.flush all via config', function (t) { + t.plan(1); + db.init(client, schema, null) + .then(function () { return db.flush(client, schema) }) + .then(function () { return client.query('SELECT * FROM ' + testTab + ';') }) + .catch(function (err) { return t.ok(err, 'selectin flushed table errors') }) + ; +}); + +test('db.flush all via options', function (t) { + t.plan(2); + db.init(client, multipleSchema, null) + .then(function () { + return db.flush(client, null, { tableName: 'table_2' }); + }) + .then(function () { return client.query('SELECT * FROM table_1;') }) + .then(function (res) { + t.ok(res, 'table_1 remians'); + + return client.query('SELECT * FROM table_2;'); + }) + .catch(function (err) { return t.ok(err, 'selectin flushed table errors') }) + ; +}); + test('close test DB connections', function (t) { client.end(t.end); }); diff --git a/test/index.test.js b/test/index.test.js index a403c08..321e605 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -51,7 +51,7 @@ test('Can register DB plugin with `schemaPath` option', function (t) { test('db handlers exposed', function (t) { var handlers = Object.keys(plugin.handlers); - var wanted = ['insert', 'select', 'delete', 'update', 'init']; + var wanted = ['insert', 'select', 'delete', 'update', 'init', 'flush']; t.ok( wanted.reduce(function (truth, handler) { diff --git a/test/istantiate_db.test.js b/test/istantiate_db.test.js index 603b9b1..0c1bf69 100644 --- a/test/istantiate_db.test.js +++ b/test/istantiate_db.test.js @@ -13,7 +13,7 @@ var testInsert = { dob: '2001-09-27', username: 'test' }; -var testTab = schema.table_name; +var testTab = schema.tableName; test('instantiateDb gives obj w/ methods bound to pg.Pool to cb', function (t) { instantiateDb(pool, schema, function (err, db) { diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js index 1c65784..180f3f2 100644 --- a/test/sql_gen.test.js +++ b/test/sql_gen.test.js @@ -29,7 +29,7 @@ tape('::init - generate SQL to create a table if none exists', function (t) { tape('::select - generate SQL to select columns from a table', function (t) { var query = sqlGen.select({ - tableName: schema.table_name, + tableName: schema.tableName, select: ['email', 'dob'] }); @@ -44,7 +44,7 @@ tape('::select - generate SQL to select columns from a table', function (t) { tape('::select - gen. SQL to select cols from table w/ where', function (t) { var query = sqlGen.select({ - tableName: schema.table_name, + tableName: schema.tableName, select: ['email', 'dob'], where: { foo: 'bar' } }); @@ -60,7 +60,7 @@ tape('::select - gen. SQL to select cols from table w/ where', function (t) { tape('::insert - generate SQL to insert a column into a table', function (t) { var query = sqlGen.insert( - { tableName: schema.table_name, fields: { email: 'me@poop.com' } } + { tableName: schema.tableName, fields: { email: 'me@poop.com' } } ); t.equal( @@ -77,7 +77,7 @@ tape('::insert - generate SQL to insert a column into a table', function (t) { }); tape('::insert - generate SQL to insert blank col into table', function (t) { - var query = sqlGen.insert({ tableName: schema.table_name }); + var query = sqlGen.insert({ tableName: schema.tableName }); t.equal( query[0], @@ -94,7 +94,7 @@ tape('::insert - generate SQL to insert blank col into table', function (t) { tape('::update - generate SQL to update a column in a table', function (t) { var query = sqlGen.update({ - tableName: schema.table_name, + tableName: schema.tableName, fields: { email: 'me@poop.com' } }); @@ -112,7 +112,7 @@ tape('::update - generate SQL to update a column in a table', function (t) { }); tape('::update - generate SQL to update no fields of column', function (t) { - var query = sqlGen.update({ tableName: schema.table_name }); + var query = sqlGen.update({ tableName: schema.tableName }); t.equal( query[0], @@ -129,7 +129,7 @@ tape('::update - generate SQL to update no fields of column', function (t) { tape('::update - gen. SQL to update a col in table w/ where', function (t) { var query = sqlGen.update({ - tableName: schema.table_name, + tableName: schema.tableName, fields: { email: 'me@poop.com' }, where: { foo: 'bar' } }); @@ -149,7 +149,7 @@ tape('::update - gen. SQL to update a col in table w/ where', function (t) { tape('::delete should generate SQL to delete a row from a table', function (t) { var query = sqlGen.delete({ - tableName: schema.table_name, + tableName: schema.tableName, where: { username: 'bob' } }); @@ -168,7 +168,7 @@ tape('::delete should generate SQL to delete a row from a table', function (t) { tape('::delete should gen SQL to delete row w/ multiple where', function (t) { var query = sqlGen.delete({ - tableName: schema.table_name, + tableName: schema.tableName, where: { username: 'bob', dob: '20/04/1988' } }); @@ -184,3 +184,14 @@ tape('::delete should gen SQL to delete row w/ multiple where', function (t) { ); t.end(); }); + +tape('::dropTable should gen SQL to drop table', function (t) { + var query = sqlGen.dropTable({ tableName: schema.tableName }); + + t.equal( + query, + 'DROP TABLE "user_data";', + 'Generate parameterised query' + ); + t.end(); +}); From 6785621f735e634a4fccebed3c9dfd8850254950 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Fri, 28 Oct 2016 11:47:32 -0500 Subject: [PATCH 07/22] :bug: make sure right db conenction being passed + ALso make sure type required in config object --- lib/config_validator.js | 2 +- lib/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/config_validator.js b/lib/config_validator.js index 524ce84..a94aad1 100644 --- a/lib/config_validator.js +++ b/lib/config_validator.js @@ -9,7 +9,7 @@ var dbNameRegEx = /^[A-Za-z_]\w{0,62}$/; var fieldTypes = Object.keys(mapObj); var fieldSchema = Joi.object() - .keys({ type: Joi.any().valid(fieldTypes) }) + .keys({ type: Joi.any().valid(fieldTypes).required() }) .unknown() ; var tableSchema = Joi.object().keys({ diff --git a/lib/index.js b/lib/index.js index 0536142..233dbed 100644 --- a/lib/index.js +++ b/lib/index.js @@ -19,7 +19,7 @@ var handlers = require('./db_handlers.js'); exports.register = function (server, options, next) { var schema = parseOptions.schema(options, server); - var connection = parseOptions.dbConfig(options.dbConnection); + var connection = parseOptions.dbConfig(options); var pool = new pg.Pool(connection); configValidator(schema); From 84d8bda14df17aabb6d265770eec1eadd3625040 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Sun, 30 Oct 2016 13:44:15 -0400 Subject: [PATCH 08/22] :bug: to tables with same unqiue field clash fix --- lib/create_table_map.js | 4 ++-- lib/sql_gen.js | 2 +- test/create_table_map.test.js | 4 ++-- test/sql_gen.test.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/create_table_map.js b/lib/create_table_map.js index 55855f4..0e0b14d 100644 --- a/lib/create_table_map.js +++ b/lib/create_table_map.js @@ -17,12 +17,12 @@ var mapObj = { } }; -function mapper (name, type, options) { +function mapper (name, type, options, tableName) { var opts = options || {}; var constraints = ''; if (opts.unique) { - constraints += ' CONSTRAINT ' + name + '_unique UNIQUE'; + constraints += ' CONSTRAINT ' + tableName + '_' + name + '_unique UNIQUE'; } return name + ' ' + mapObj[type](opts) + constraints; diff --git a/lib/sql_gen.js b/lib/sql_gen.js index 1c5597b..d6641ca 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -37,7 +37,7 @@ exports.init = function init (config) { var type = fields[key].type; var opts = _.except(['type'], fields[key]); - return mapper(key, type, opts); + return mapper(key, type, opts, tableName); }); return ['CREATE TABLE IF NOT EXISTS "' + tableName + '"'] diff --git a/test/create_table_map.test.js b/test/create_table_map.test.js index 46c4674..b755646 100644 --- a/test/create_table_map.test.js +++ b/test/create_table_map.test.js @@ -77,8 +77,8 @@ test('Create Table Mapper Function w/ no options', function (t) { test('Create Table Mapper Function w/ unique option', function (t) { t.equal( - mapper('email', 'string', { unique: true }), - 'email VARCHAR(80) CONSTRAINT email_unique UNIQUE', + mapper('email', 'string', { unique: true }, 'test_table'), + 'email VARCHAR(80) CONSTRAINT test_table_email_unique UNIQUE', 'constraint added to column' ); t.end(); diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js index 180f3f2..14c4027 100644 --- a/test/sql_gen.test.js +++ b/test/sql_gen.test.js @@ -19,7 +19,7 @@ tape('::init - generate SQL to create a table if none exists', function (t) { query, 'CREATE TABLE IF NOT EXISTS "user_data" (' + 'email VARCHAR(80), ' - + 'username VARCHAR(20) CONSTRAINT username_unique UNIQUE, ' + + 'username VARCHAR(20) CONSTRAINT user_data_username_unique UNIQUE, ' + 'dob DATE' + ')', 'Create table query generation from config object' From 44c6ef8cebffd603a6cfe52d5297d21fa92ab682 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Sun, 30 Oct 2016 13:53:08 -0400 Subject: [PATCH 09/22] :heavy_plus_sign: expose validate and createClient helpers #10 #11 --- lib/index.js | 8 ++++++++ test/index.test.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/index.js b/lib/index.js index 233dbed..71a876a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -41,3 +41,11 @@ exports.register = function (server, options, next) { exports.register.attributes = { name: 'abase-db' }; exports.handlers = handlers; + +exports.validate = configValidator; + +exports.createClient = function (dbConnection) { + var connection = parseOptions.dbConfig({ dbConnection: dbConnection }); + + return new pg.Client(connection); +}; diff --git a/test/index.test.js b/test/index.test.js index 321e605..2dc7ef9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -61,3 +61,18 @@ test('db handlers exposed', function (t) { ); t.end(); }); + +test('validate exposed', function (t) { + t.ok( + typeof plugin.validate === 'function', + 'validate function given' + ); + t.end(); +}); + +test('createClient helper', function (t) { + var testClient = plugin.createClient(process.env.TEST_DATABASE_URL); + + t.ok(testClient.connection, 'client object returned'); + t.end(); +}); From 1b1308b0735d18f961d62a5acec03d1ac960acb6 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Mon, 31 Oct 2016 00:25:45 -0400 Subject: [PATCH 10/22] 0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e41806..4d9228b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "0.1.0", + "version": "0.2.0", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/index.js", "devDependencies": { From 89316b1f1f7ca621dd07d453d6fe2e1fd8fb63cd Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 1 Nov 2016 11:43:42 -0400 Subject: [PATCH 11/22] :memo: Add example to repo #6 --- example/index.js | 34 ++++++++++++++++++++++ example/routes.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++ example/schema.js | 10 +++++++ package.json | 9 +++--- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 example/index.js create mode 100644 example/routes.js create mode 100644 example/schema.js diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..64a534d --- /dev/null +++ b/example/index.js @@ -0,0 +1,34 @@ +'use strict'; + +var env = require('env2')('config.env'); // eslint-disable-line + +var Hapi = require('hapi'); +var hoek = require('hoek'); +var AbaseDb = require('../lib/'); +var routes = require('./routes.js'); +var dbSchema = require('./schema.js'); + +var server = new Hapi.Server(); + +var abaseDbOptions = { + dbConnection: process.env.TEST_DATABASE_URL, + schema: dbSchema +}; + +server.connection({ port: 8000 }); + +server.register([ + { register: AbaseDb, options: abaseDbOptions } +], function (err) { + hoek.assert(!err, err); + + server.route(routes); + + server.start(function (error) { + hoek.assert(!error, error); + + console.log('Visit: http://localhost:' + server.info.port + '/'); // eslint-disable-line + }); +}); + +module.exports = server; diff --git a/example/routes.js b/example/routes.js new file mode 100644 index 0000000..8f43651 --- /dev/null +++ b/example/routes.js @@ -0,0 +1,72 @@ +'use strict'; + +var newPost = '
' + + '' + + '' + + '' + + '
' +; + +function existingPost (post) { + var id = post.id; + var title = post.title; + var body = post.body; + + return '
' + + '' + + '' + + '' + + '
' + + '
' + + '' + + '
' + ; +} + +module.exports = [{ + method: 'GET', + path: '/', + handler: function (request, reply) { + return request.abase.db.select({ tableName: 'posts' }, function (_, data) { + var sortedRows = data.rows.sort(function (a, b) { + return a.id > b.id; + }); + + return reply(newPost + sortedRows.map(existingPost).join('
')); + }); + } +}, { + method: 'POST', + path: '/new', + handler: function (request, reply) { + var id = Date.now(); + var fields = Object.assign({ id: id }, request.payload); + + return request.abase.db.insert( + { tableName: 'posts', fields: fields }, + function () { return reply.redirect('/') } + ); + } +}, { + method: 'GET', + path: '/delete/{id}', + handler: function (request, reply) { + var id = request.params.id; + + return request.abase.db.delete( + { tableName: 'posts', where: { id: id } }, + function () { return reply.redirect('/') } + ); + } +}, { + method: 'POST', + path: '/update/{id}', + handler: function (request, reply) { + var id = request.params.id; + + return request.abase.db.update( + { tableName: 'posts', where: { id: id }, fields: request.payload }, + function () { return reply.redirect('/') } + ); + } +}]; diff --git a/example/schema.js b/example/schema.js new file mode 100644 index 0000000..68ac0dc --- /dev/null +++ b/example/schema.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { + tableName: 'posts', + fields: { + title: { type: 'string' }, + body: { type: 'string' }, + id: { type: 'number', integer: true } + } +}; diff --git a/package.json b/package.json index 4d9228b..f0adac7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "abase-db", "version": "0.2.0", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", - "main": "lib/index.js", + "main": "lib/", "devDependencies": { "goodparts": "^1.1.0", "istanbul": "^0.4.5", @@ -17,12 +17,13 @@ "pg": "^6.1.0" }, "scripts": { - "test": "tape ./test/**/*.test.js", + "test": "tape './test/**/*.test.js'", "lint": "node_modules/.bin/goodparts .", "lint:fix": "node_modules/.bin/goodparts . --fix", - "cover": "node_modules/.bin/istanbul cover node_modules/.bin/tape ./test/*.test.js", + "cover": "node_modules/.bin/istanbul cover node_modules/.bin/tape './test/*.test.js'", "check-coverage": "node_modules/.bin/istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100", - "start": "node lib/index.js" + "run:example": "node example/" + }, "repository": { "type": "git", From 65f98c93cdb32f130312bb1c5473477c3c36057c Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 1 Nov 2016 12:01:27 -0400 Subject: [PATCH 12/22] :green_heart: Add CI #3 --- .travis.yml | 8 ++++++++ README.md | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a2430ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - "4" + - "6" +before_install: + - pip install --user codecov +after_success: + - codecov --file coverage/lcov.info --disable search diff --git a/README.md b/README.md index 5ddf854..b8ab5ee 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # joi-postgresql A little experiment in defining models in Joi and creating PostgreSQL Tables + +[![Build Status](https://travis-ci.org/dwyl/joi-postgresql.svg?branch=master)](https://travis-ci.org/dwyl/joi-postgresql) +[![codecov](https://codecov.io/gh/dwyl/joi-postgresql/branch/master/graph/badge.svg)](https://codecov.io/gh/dwyl/joi-postgresql) +[![Code Climate](https://codeclimate.com/github/dwyl/joi-postgresql/badges/gpa.svg)](https://codeclimate.com/github/dwyl/joi-postgresql) +[![dependencies Status](https://david-dm.org/dwyl/joi-postgresql/status.svg)](https://david-dm.org/dwyl/joi-postgresql) +[![devDependencies Status](https://david-dm.org/dwyl/joi-postgresql/dev-status.svg)](https://david-dm.org/dwyl/joi-postgresql?type=dev) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/joi-postgresql/issues) From 392b943551659313042bfa2a31e4280342bdbbcb Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 1 Nov 2016 19:05:37 -0400 Subject: [PATCH 13/22] :memo: Add complete documentation for work so far #8 --- README.md | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b8ab5ee..3252017 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,184 @@ A little experiment in defining models in Joi and creating PostgreSQL Tables [![dependencies Status](https://david-dm.org/dwyl/joi-postgresql/status.svg)](https://david-dm.org/dwyl/joi-postgresql) [![devDependencies Status](https://david-dm.org/dwyl/joi-postgresql/dev-status.svg)](https://david-dm.org/dwyl/joi-postgresql?type=dev) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/joi-postgresql/issues) + +## abase-db + +### What? +abase-db is a [hapi](https://github.com/hapijs/hapi) plugin that provides an easy way to set up postgres database tables and perform CRUD operations by declaring a schema object which is heavily influenced by [joi](https://github.com/hapijs/joi). + +It can be used alone but is most powerful when used as part of [abase](https://github.com/dwyl/abase) or with your select few abase plugins. + +Note if you are totally new to Hapi.js see: https://github.com/dwyl/learn-hapi +And/or if you are new to postgres check out: https://github.com/dwyl/learn-postgresql + +### Why? + +From a joi schema we should be able to infer many things about the fields in a database. `abase-db` provides the mapping between a config (inspired by joi schema) to commands that can create tables with the correct fields. + +We also want a "plug and play" access and easy to use handlers to perform CRUD operations and `abase-db` offers this without having to worry about any postgres querying. + +For more understanding of *why* see the parent module [abase]((https://github.com/dwyl/abase)) as this provides just the db part. + +> #### Why PostgreSQL? + +> While there is a lot of hype surrounding NoSQL Databases like MongoDB & Redis, we found we were having to write a lot of code to do useful queries. And while de-normalising data might "make sense" for "scalability" in theory, what we found in practice is that even with 100 Billion Records (way more users than 99.99% of companies/startups!) a well-managed PostgreSQL cluster copes very well. + +> Make up your own mind: https://www.postgresql.org/about +If you're still Curious or Worried about scaling PostgreSQL? see: https://www.citusdata.com Want to model the network of people as a graph? https://github.com/cayleygraph/cayley + +### How? + +1. Install `npm install abase-db --save` +2. Write a schema for your tables like so: +```js + var schema = { + tableName: 'users', + fields: { + name: { type: 'string' } + } + } +``` +3. Run a database remotely or locally (see [here](https://github.com/dwyl/learn-postgresql) for how) and acquire db url or connection object. +4. Create options object of the form: +```js + var options = { + dbConnection: process.env.DATABASE_URL, + schema: dbSchema + }; +``` +5. Plugin +```js +server.register([ + { register: require('abase-db'), options: options } +], function () { + server.route(routes); + server.start(); +}); +``` +6. Play +```js +handler: function (request, reply) { + return request.abase.db.insert( + { tableName: 'users', fields: request.payload }, + function () { return reply('OK') } + ); +} +``` +7. Play without hapi. See API section below. + +### API + +#### Plugin: `require('abase-db')` + +##### Registration +When registered with Hapi takes options of the form: +``` + { dbConnection, schema } +``` +###### dbConnection +Either provide a database url and we'll do the rest or an object that can used to configure a pooled connection with [node-pg](https://github.com/brianc/node-postgres#client-pooling). +###### Schema + +The schema is in align with the requirements made by [abase]((https://github.com/dwyl/abase)) and as stated before is inspired by joi and will try to provide a one to one mapping. + +The schema must be an object (or an array of objects for multiple tables) of the form: `{ tableName, fields }`. + +`fields` is of the form `{ [afieldName]: { type, rest: optional }` + +Table and field names must be valid postgres table and column names. (non empty, alphanumeric, no leading number, less than 64) + +Each field must be given a type prop. Data/joi types we support: + +| Joi type (type prop for field)| Postgres type | Notes | +|---|---|---| +| `date` | `DATE` or `TIMESTAMP` | set `timestamp: true` for latter | +| `number` | `DOUBLE PRECISION` or `BIGINT` | set `integer: true` for latter | +| `string` | `VARCHAR(80 or max)` | `80` default, set `max: 123` as you like for more/less | +|boolean | BOOLEAN | | + +More information can be inferred from `lib/config_validator.js` + +Each field can also take more properties most of which will be used by other abase modules and have no effect but the ones we care about right now are. + +| Property | Notes | +|---|---| +| `unique` | set to `true` if you want column unique | +| `max`, `timestamp`, `integer` | see types table above for relevance | + +##### Under the hood + +###### Table Set Up +With given database and schema, on initialisation of plugin, we will create all necessary tables if they don't already exist. + +This will only therefore happen if starting server for the first time, or if a new table is added to the schema. + +**Unfortunately** if you want to modify a tables schema you will have to drop the whole table to have the database reconfigured on start up. We look to find a nice process for this in the future if you want to update your tables with new columns. + +###### Request decoration + +Each request will have the db handlers `insert`, `select`, `update`, `delete`. They all have clients attached and ready to go. + +They can be accessed like so: `request.abase.db.insert`. + +They are all of the form `function(options, callback = optional)` and return promises if no callback given. + +The `options` object must contain `tableName`, i.e. the table you want to operate on. Below are more details for properties of options. + +| Property | Used in | Notes | +| --- | --- | --- | +| `fields` | `insert`, `update` | Object with field names and values corresponding to the schema provided | +| `select` | `select` | array of keys which want to be retrieved, if not present defaults to all columns | +| `where` | `select`, `update`, `delete` | object with field names and values that must match by equality (would like inequality in future) | + +###### Server decoration + +The hapi server will be given a method `endAbaseDb` of the form `function (callback)` which can be called to closed the pool connection. + +##### use + +#### validate: `require('abase-db').validate` + +Helper that you can use to check your schema outside of hapi. Takes in a schema object and will throw if it fails. + +#### createConnection: `require('abase').createConnection` + +Helper of the form `function(dbConnection)` to create a single node-pg client that is configured in the same way as how you provide your dbConnection above. + +#### handlers: `require('abase').handlers` + +Object with methods `insert`, `select`, `update`, `delete`, `init`, `flush`. + +They all have form `function(client, schema, options, cb)` so you will have to bind your own client. + +Crud operation documented above. + +##### init +Used at plugin registration takes same schema and doesn't use options arg. + +##### flush +Used to drop tables easily. If given options arg will delete on a table by table basis but if left out will delete all tables in schema. +options takes the form `{tableName}`. + +### Examples and tests + +#### setup + +For examples and tests you will need a `config.env` file at the root of your project with a test database url like so: +``` +TEST_DATABASE_URL=psql://localhost:5432/testdb +``` + +Note: this database must be running and before tests are run the tables may be removed from the database so don't keep anything important there. + +#### Simple example + +To see a simple example in action type `npm run example` into your command line. + +### Questions and Suggestions + +We hope you find this module useful! + +If you need something cleared up, have any requests or want to offer any improvements then please create an issue or better yet a PR! + +Note We are aware that not all postgres features may be supported yet. This module will need a few iterations so please suggest missing features to be implemented as you use it and we can hopefully work together to solve it. diff --git a/package.json b/package.json index f0adac7..905faeb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "lint:fix": "node_modules/.bin/goodparts . --fix", "cover": "node_modules/.bin/istanbul cover node_modules/.bin/tape './test/*.test.js'", "check-coverage": "node_modules/.bin/istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100", - "run:example": "node example/" + "example": "node example/" }, "repository": { From 75f99889b8bd2e6b427dd51ba7f6047f6c5c5dbf Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 1 Nov 2016 19:16:28 -0400 Subject: [PATCH 14/22] :green_heart: add database setup to travis #3 --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index a2430ee..3f91ac9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: node_js node_js: - "4" - "6" +services: + - postgresql +before_script: + - psql -c 'create database testdb;' -U postgres before_install: - pip install --user codecov after_success: From f18c787aaaaff9ee0cc00034ca51d441c0955e46 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 11:36:54 +0000 Subject: [PATCH 15/22] :heavy_plus_sign: type id #12 --- example_schema.js | 3 ++- lib/config_validator.js | 6 +++++- lib/create_table_map.js | 3 +++ lib/db_handlers.js | 4 ++-- lib/sql_gen.js | 37 ++++++++++++++++++++++++++----------- package.json | 2 +- test/index.test.js | 2 +- test/istantiate_db.test.js | 1 + test/sql_gen.test.js | 23 ++++++++++++++--------- 9 files changed, 55 insertions(+), 26 deletions(-) diff --git a/example_schema.js b/example_schema.js index 915c5b7..f557b94 100644 --- a/example_schema.js +++ b/example_schema.js @@ -13,6 +13,7 @@ module.exports = { max: 20, unique: true }, - dob: { type: 'date' } + dob: { type: 'date' }, + id: { type: 'id' } } }; diff --git a/lib/config_validator.js b/lib/config_validator.js index a94aad1..8bf4812 100644 --- a/lib/config_validator.js +++ b/lib/config_validator.js @@ -8,8 +8,12 @@ var mapObj = require('./create_table_map.js').mapObj; var dbNameRegEx = /^[A-Za-z_]\w{0,62}$/; var fieldTypes = Object.keys(mapObj); +var typeSchema = Joi.any() + .valid(fieldTypes) + .required() +; var fieldSchema = Joi.object() - .keys({ type: Joi.any().valid(fieldTypes).required() }) + .keys({ type: typeSchema }) .unknown() ; var tableSchema = Joi.object().keys({ diff --git a/lib/create_table_map.js b/lib/create_table_map.js index 0e0b14d..6f0001c 100644 --- a/lib/create_table_map.js +++ b/lib/create_table_map.js @@ -1,6 +1,9 @@ 'use strict'; var mapObj = { + id: function () { + return 'VARCHAR(36)'; + }, number: function (opts) { return opts.integer ? 'BIGINT' : 'DOUBLE PRECISION'; }, diff --git a/lib/db_handlers.js b/lib/db_handlers.js index 9918b05..006df70 100644 --- a/lib/db_handlers.js +++ b/lib/db_handlers.js @@ -29,8 +29,8 @@ methods.flush = function (client, config, options, cb) { }; ['select', 'update', 'delete', 'insert'].forEach(function (method) { - methods[method] = function (client, _, options, cb) { - var args = sqlGen[method](options).concat([cb]); + methods[method] = function (client, config, options, cb) { + var args = sqlGen[method](config, options).concat([cb]); return client.query.apply(client, args); }; diff --git a/lib/sql_gen.js b/lib/sql_gen.js index d6641ca..d5702c6 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -1,7 +1,9 @@ 'use strict'; +var aguid = require('aguid'); + var mapper = require('./create_table_map.js'); -var _ = require('./utils.js'); +var utils = require('./utils.js'); function paramStr (columns, opts) { @@ -20,7 +22,7 @@ function paramStr (columns, opts) { function processWhere (where, query, values) { var keys = Object.keys(where); var conds = paramStr(keys, { offset: values.length, assign: true }); - var vals = _.values(where, keys); + var vals = utils.values(where, keys); return { query: query.concat('WHERE').concat(conds.join(' AND ')), @@ -35,7 +37,7 @@ exports.init = function init (config) { var columns = Object.keys(fields).map(function (key) { var type = fields[key].type; - var opts = _.except(['type'], fields[key]); + var opts = utils.except(['type'], fields[key]); return mapper(key, type, opts, tableName); }); @@ -47,7 +49,7 @@ exports.init = function init (config) { }; -exports.select = function select (options) { +exports.select = function select (_, options) { var columns = options.select || ['*']; var values = []; var query = ['SELECT'] @@ -68,28 +70,41 @@ exports.select = function select (options) { }; -exports.insert = function insert (options) { +exports.insert = function insert (config, options) { var fields = options.fields || {}; - var columns = Object.keys(fields); - var values = _.values(fields, columns); + var tableConfig = [] + .concat(config) + .filter(function (table) { + return table.tableName === options.tableName; + })[0] + ; + var idFields = Object.keys(tableConfig.fields).filter(function (field) { + return tableConfig.fields[field].type === 'id'; + }); + var ids = idFields.map(aguid); + var normalColumns = Object.keys(fields); + var values = utils.values(fields, normalColumns).concat(ids); + var columns = normalColumns.concat(idFields); var params = paramStr(columns); + var query = ['INSERT INTO "' + options.tableName + '"'] .concat('(' + columns.join(', ') + ')') .concat('VALUES') .concat('(' + params.join(', ') + ')') .join(' ') - .trim(); + .trim() + ; return [query, values]; }; -exports.update = function update (options) { +exports.update = function update (_, options) { var fields = options.fields || {}; var columns = Object.keys(fields); var conditions = paramStr(columns, { assign: true }); - var values = _.values(fields, columns); + var values = utils.values(fields, columns); var query = ['UPDATE "' + options.tableName + '"'] .concat('SET') @@ -108,7 +123,7 @@ exports.update = function update (options) { }; -exports.delete = function _delete (options) { +exports.delete = function del (_, options) { var query = ['DELETE FROM "' + options.tableName + '"']; var values = []; var result = processWhere(options.where, query, values); diff --git a/package.json b/package.json index 905faeb..6d52588 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "hapi": "^15.1.1" }, "dependencies": { + "aguid": "^1.0.4", "env2": "^2.1.1", "hoek": "^4.1.0", "joi": "^9.0.4", @@ -23,7 +24,6 @@ "cover": "node_modules/.bin/istanbul cover node_modules/.bin/tape './test/*.test.js'", "check-coverage": "node_modules/.bin/istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100", "example": "node example/" - }, "repository": { "type": "git", diff --git a/test/index.test.js b/test/index.test.js index 2dc7ef9..c6be034 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -3,8 +3,8 @@ var test = require('tape'); var Hapi = require('hapi'); var path = require('path'); -var plugin = require('../lib/index.js'); +var plugin = require('../lib/index.js'); test('Can register DB plugin with `schemaPath` option', function (t) { var server = new Hapi.Server(); diff --git a/test/istantiate_db.test.js b/test/istantiate_db.test.js index 0c1bf69..a27c541 100644 --- a/test/istantiate_db.test.js +++ b/test/istantiate_db.test.js @@ -38,6 +38,7 @@ test('db bound .insert adds to DB :: promise interface', function (t) { }); }) .then(function (result) { + t.equal(result.rows[0].id.length, 36, 'guid generated'); t.equal(result.rows[0].email, testInsert.email, 'Email matches'); t.end(); }) diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js index 14c4027..8e6b17c 100644 --- a/test/sql_gen.test.js +++ b/test/sql_gen.test.js @@ -20,7 +20,8 @@ tape('::init - generate SQL to create a table if none exists', function (t) { 'CREATE TABLE IF NOT EXISTS "user_data" (' + 'email VARCHAR(80), ' + 'username VARCHAR(20) CONSTRAINT user_data_username_unique UNIQUE, ' - + 'dob DATE' + + 'dob DATE, ' + + 'id VARCHAR(36)' + ')', 'Create table query generation from config object' ); @@ -28,7 +29,7 @@ tape('::init - generate SQL to create a table if none exists', function (t) { }); tape('::select - generate SQL to select columns from a table', function (t) { - var query = sqlGen.select({ + var query = sqlGen.select(null, { tableName: schema.tableName, select: ['email', 'dob'] }); @@ -43,7 +44,7 @@ tape('::select - generate SQL to select columns from a table', function (t) { }); tape('::select - gen. SQL to select cols from table w/ where', function (t) { - var query = sqlGen.select({ + var query = sqlGen.select(null, { tableName: schema.tableName, select: ['email', 'dob'], where: { foo: 'bar' } @@ -60,6 +61,7 @@ tape('::select - gen. SQL to select cols from table w/ where', function (t) { tape('::insert - generate SQL to insert a column into a table', function (t) { var query = sqlGen.insert( + { tableName: schema.tableName, fields: {} }, { tableName: schema.tableName, fields: { email: 'me@poop.com' } } ); @@ -77,7 +79,10 @@ tape('::insert - generate SQL to insert a column into a table', function (t) { }); tape('::insert - generate SQL to insert blank col into table', function (t) { - var query = sqlGen.insert({ tableName: schema.tableName }); + var query = sqlGen.insert( + { tableName: schema.tableName, fields: {} }, + { tableName: schema.tableName } + ); t.equal( query[0], @@ -93,7 +98,7 @@ tape('::insert - generate SQL to insert blank col into table', function (t) { }); tape('::update - generate SQL to update a column in a table', function (t) { - var query = sqlGen.update({ + var query = sqlGen.update(null, { tableName: schema.tableName, fields: { email: 'me@poop.com' } }); @@ -112,7 +117,7 @@ tape('::update - generate SQL to update a column in a table', function (t) { }); tape('::update - generate SQL to update no fields of column', function (t) { - var query = sqlGen.update({ tableName: schema.tableName }); + var query = sqlGen.update(null, { tableName: schema.tableName }); t.equal( query[0], @@ -128,7 +133,7 @@ tape('::update - generate SQL to update no fields of column', function (t) { }); tape('::update - gen. SQL to update a col in table w/ where', function (t) { - var query = sqlGen.update({ + var query = sqlGen.update(null, { tableName: schema.tableName, fields: { email: 'me@poop.com' }, where: { foo: 'bar' } @@ -148,7 +153,7 @@ tape('::update - gen. SQL to update a col in table w/ where', function (t) { }); tape('::delete should generate SQL to delete a row from a table', function (t) { - var query = sqlGen.delete({ + var query = sqlGen.delete(null, { tableName: schema.tableName, where: { username: 'bob' } }); @@ -167,7 +172,7 @@ tape('::delete should generate SQL to delete a row from a table', function (t) { }); tape('::delete should gen SQL to delete row w/ multiple where', function (t) { - var query = sqlGen.delete({ + var query = sqlGen.delete(null, { tableName: schema.tableName, where: { username: 'bob', dob: '20/04/1988' } }); From 6b69a1346214f833db2206adeda9ec77fa49199d Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 11:47:21 +0000 Subject: [PATCH 16/22] :heavy_plus_sign: primary key constraint #12 --- lib/create_table_map.js | 3 +++ test/create_table_map.test.js | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/create_table_map.js b/lib/create_table_map.js index 6f0001c..85e0d72 100644 --- a/lib/create_table_map.js +++ b/lib/create_table_map.js @@ -24,6 +24,9 @@ function mapper (name, type, options, tableName) { var opts = options || {}; var constraints = ''; + if (opts.primaryKey) { + constraints += ' CONSTRAINT ' + tableName + '_pk PRIMARY KEY'; + } if (opts.unique) { constraints += ' CONSTRAINT ' + tableName + '_' + name + '_unique UNIQUE'; } diff --git a/test/create_table_map.test.js b/test/create_table_map.test.js index b755646..bd283bb 100644 --- a/test/create_table_map.test.js +++ b/test/create_table_map.test.js @@ -83,3 +83,12 @@ test('Create Table Mapper Function w/ unique option', function (t) { ); t.end(); }); + +test('Create Table Mapper Function w/ primaryKey option', function (t) { + t.equal( + mapper('email', 'string', { primaryKey: true }, 'test_table'), + 'email VARCHAR(80) CONSTRAINT test_table_pk PRIMARY KEY', + 'pk constraint added to column' + ); + t.end(); +}); From 028eb600d0ac3142539727074f77045ac33d244f Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 11:56:46 +0000 Subject: [PATCH 17/22] :memo: Readme update for id and primarykey #12 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3252017..a6b7346 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ The schema is in align with the requirements made by [abase]((https://github.com The schema must be an object (or an array of objects for multiple tables) of the form: `{ tableName, fields }`. -`fields` is of the form `{ [afieldName]: { type, rest: optional }` +`fields` is of the form `{ [fieldName]: { type, rest: optional }` Table and field names must be valid postgres table and column names. (non empty, alphanumeric, no leading number, less than 64) @@ -102,6 +102,7 @@ Each field must be given a type prop. Data/joi types we support: | `number` | `DOUBLE PRECISION` or `BIGINT` | set `integer: true` for latter | | `string` | `VARCHAR(80 or max)` | `80` default, set `max: 123` as you like for more/less | |boolean | BOOLEAN | | +| `id` | VARCHAR(36) | **warning** if using this type do not add this field to your insert, we will generate an id on each insertion (Generated with [aguid](https://github.com/dwyl/aguid)) | More information can be inferred from `lib/config_validator.js` @@ -110,6 +111,7 @@ Each field can also take more properties most of which will be used by other aba | Property | Notes | |---|---| | `unique` | set to `true` if you want column unique | +| `primaryKey` | set to `true` if you want this field to act as your primary key (note only one field allowed!) | | `max`, `timestamp`, `integer` | see types table above for relevance | ##### Under the hood From e659b26be7b82a2093bb7da2bf37003fd83d54a7 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 11:58:01 +0000 Subject: [PATCH 18/22] 0.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d52588..f294f80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "0.2.0", + "version": "0.2.1", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/", "devDependencies": { From 4bc965a8e2df069abbaece0090746b72bc83cf2f Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 12:31:17 +0000 Subject: [PATCH 19/22] :bug: ensure blank arg passed to aguid to guarentee uniquness --- lib/sql_gen.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/sql_gen.js b/lib/sql_gen.js index d5702c6..29647e2 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -81,7 +81,9 @@ exports.insert = function insert (config, options) { var idFields = Object.keys(tableConfig.fields).filter(function (field) { return tableConfig.fields[field].type === 'id'; }); - var ids = idFields.map(aguid); + var ids = idFields.map(function () { + return aguid(); + }); var normalColumns = Object.keys(fields); var values = utils.values(fields, normalColumns).concat(ids); var columns = normalColumns.concat(idFields); From f00114f8dd5c4c321fabf4b430755f472144b4b7 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 12:31:30 +0000 Subject: [PATCH 20/22] 0.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f294f80..ad8f07b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "0.2.1", + "version": "0.2.2", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/", "devDependencies": { From 8739d1686636966757d91da853b6aad6f62a67b6 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 13:50:54 +0000 Subject: [PATCH 21/22] :heavy_plus_sign: Return generated ids in the response #12 --- lib/sql_gen.js | 1 + test/db_handlers.test.js | 4 +++- test/sql_gen.test.js | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/sql_gen.js b/lib/sql_gen.js index 29647e2..c140011 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -96,6 +96,7 @@ exports.insert = function insert (config, options) { .concat('(' + params.join(', ') + ')') .join(' ') .trim() + + ' RETURNING ' + '(' + idFields.join(', ') + ')' ; return [query, values]; diff --git a/test/db_handlers.test.js b/test/db_handlers.test.js index c29ca23..e66aaa2 100644 --- a/test/db_handlers.test.js +++ b/test/db_handlers.test.js @@ -67,7 +67,9 @@ test('db.init multiple tables', function (t) { test('db.insert & default select w custom where', function (t) { db.insert(client, schema, { fields: testInsert, tableName: testTab }) - .then(function () { + .then(function (res) { + t.ok(res.rows[0].id, 'id returned in response'); + return db.select(client, schema, { where: { dob: '2001-09-27' }, tableName: testTab diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js index 8e6b17c..4bc4f62 100644 --- a/test/sql_gen.test.js +++ b/test/sql_gen.test.js @@ -67,7 +67,7 @@ tape('::insert - generate SQL to insert a column into a table', function (t) { t.equal( query[0], - 'INSERT INTO "user_data" (email) VALUES ($1)', + 'INSERT INTO "user_data" (email) VALUES ($1) RETURNING ()', 'Generate parameterised query' ); t.deepEqual( @@ -86,7 +86,7 @@ tape('::insert - generate SQL to insert blank col into table', function (t) { t.equal( query[0], - 'INSERT INTO "user_data" () VALUES ()', + 'INSERT INTO "user_data" () VALUES () RETURNING ()', 'Generate query for blank line' ); t.deepEqual( From e35f05f1a1261c0d018d1114119248ffb8948f97 Mon Sep 17 00:00:00 2001 From: Jack Rans Date: Tue, 15 Nov 2016 13:51:01 +0000 Subject: [PATCH 22/22] 0.2.3 --- lib/sql_gen.js | 2 -- package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/sql_gen.js b/lib/sql_gen.js index c140011..b3d3d3a 100644 --- a/lib/sql_gen.js +++ b/lib/sql_gen.js @@ -88,8 +88,6 @@ exports.insert = function insert (config, options) { var values = utils.values(fields, normalColumns).concat(ids); var columns = normalColumns.concat(idFields); var params = paramStr(columns); - - var query = ['INSERT INTO "' + options.tableName + '"'] .concat('(' + columns.join(', ') + ')') .concat('VALUES') diff --git a/package.json b/package.json index ad8f07b..7a4bb86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "abase-db", - "version": "0.2.2", + "version": "0.2.3", "description": "A little experiment in defining models in Joi and creating PostgreSQL Tables", "main": "lib/", "devDependencies": {