From 22399a0cad56252714d94aaee4a8d4ba00bb6603 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Oct 2016 21:22:49 +0530 Subject: [PATCH] feat(database): add support for table prefixing Closes #58 --- src/Database/index.js | 57 ++++++++++++++++++++++ src/Lucid/Model/index.js | 22 +++++++++ src/Lucid/QueryBuilder/index.js | 11 ++++- test/unit/database.spec.js | 65 +++++++++++++++++++++++++ test/unit/helpers/config.js | 59 ++++++++++++---------- test/unit/helpers/mysqlConnections.js | 10 ++++ test/unit/helpers/postgresConnection.js | 10 ++++ test/unit/helpers/sqliteConnections.js | 10 ++++ test/unit/lucid.spec.js | 53 ++++++++++++++++++++ 9 files changed, 271 insertions(+), 26 deletions(-) diff --git a/src/Database/index.js b/src/Database/index.js index fe0e5609..52d4409d 100644 --- a/src/Database/index.js +++ b/src/Database/index.js @@ -143,6 +143,24 @@ Database.connection = function (connection) { client.client.QueryBuilder.prototype.forPage = Database.forPage client.client.QueryBuilder.prototype.paginate = Database.paginate client.client.QueryBuilder.prototype.chunk = Database.chunk + client.client.QueryBuilder.prototype._originalTable = client.client.QueryBuilder.prototype.table + client.client.QueryBuilder.prototype.table = Database.table + client.client.QueryBuilder.prototype.from = Database.table + client.client.QueryBuilder.prototype.into = Database.table + client.client.QueryBuilder.prototype.withPrefix = Database.withPrefix + client.client.QueryBuilder.prototype.withoutPrefix = Database.withoutPrefix + + /** + * Adding methods on the client if withoutPrefix or withPrefix + * is called directly it will return the query builder. + */ + client.withoutPrefix = function () { + return new this.client.QueryBuilder(this.client).withoutPrefix() + } + client.withPrefix = function (prefix) { + return new this.client.QueryBuilder(this.client).withPrefix(prefix) + } + connectionPools[connection] = client } @@ -339,6 +357,45 @@ Database.chunk = function * (limit, cb, page) { } } +/** + * Overriding the orginal knex.table method to prefix + * the table name based upon the prefix option + * defined in the config + * + * @param {String} tableName + * + * @return {Object} + */ +Database.table = function (tableName) { + const prefix = this._instancePrefix || this.client.config.prefix + const prefixedTableName = (prefix && !this._skipPrefix) ? `${prefix}${tableName}`: tableName + this._originalTable(prefixedTableName) + return this +} + +/** + * Skipping the prefix for a single query + * + * @return {Object} + */ +Database.withoutPrefix = function () { + this._skipPrefix = true + return this +} + +/** + * Changing the prefix for a given query + * + * @param {String} prefix + * + * @return {Object} + */ +Database.withPrefix = function (prefix) { + this._instancePrefix = prefix + return this +} + + /** * these methods are not proxied and instead actual implementations * are returned diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index e4cccd5f..06fe17cc 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -355,6 +355,28 @@ class Model { return util.makeTableName(this) } + /** + * Returns a custom prefix to be used for selecting the database + * table for a given model + * + * @return {String} + * + * @public + */ + static get prefix () { + return null + } + + /** + * A getter defining whether or not to skip + * table prefixing for this model. + * + * @return {Boolean} + */ + static get skipPrefix () { + return false + } + /** * primary key to be used for given table. Same key is used for fetching * associations. Defaults to id diff --git a/src/Lucid/QueryBuilder/index.js b/src/Lucid/QueryBuilder/index.js index 9a504fd3..256dd450 100644 --- a/src/Lucid/QueryBuilder/index.js +++ b/src/Lucid/QueryBuilder/index.js @@ -26,7 +26,16 @@ class QueryBuilder { const Database = Ioc.use('Adonis/Src/Database') this.HostModel = HostModel this.queryBuilder = Database.connection(this.HostModel.connection) - this.modelQueryBuilder = this.queryBuilder(this.HostModel.table) + this.modelQueryBuilder = null + + if (HostModel.prefix && !HostModel.skipPrefix) { + this.modelQueryBuilder = this.queryBuilder.withPrefix(HostModel.prefix).table(this.HostModel.table) + } else if (HostModel.skipPrefix) { + this.modelQueryBuilder = this.queryBuilder.withoutPrefix().table(this.HostModel.table) + } else { + this.modelQueryBuilder = this.queryBuilder.table(this.HostModel.table) + } + this.avoidTrashed = false this.eagerLoad = new EagerLoad() return new Proxy(this, proxyHandler) diff --git a/test/unit/database.spec.js b/test/unit/database.spec.js index c773c0e9..f3e0c8d5 100644 --- a/test/unit/database.spec.js +++ b/test/unit/database.spec.js @@ -261,4 +261,69 @@ describe('Database provider', function () { }) expect(callbackCalledForTimes).to.equal(allUsers.length) }) + + it('should be able to prefix the database table using a configuration option', function * () { + Database._setConfigProvider(config.withPrefix) + const query = Database.table('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users"')) + }) + + it('should be able to prefix the database table when table method is called after other methods', function * () { + const query = Database.where('username', 'foo').table('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users" where "username" = ?')) + }) + + it('should be able to prefix the database table when from method is used', function * () { + const query = Database.from('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users"')) + }) + + it('should be able to prefix the database table when from method is called after other methods', function * () { + const query = Database.where('username', 'foo').from('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users" where "username" = ?')) + }) + + it('should be able to prefix the database table when into method is used', function * () { + const query = Database.into('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users"')) + }) + + it('should be able to prefix the database table when into method is called after other methods', function * () { + const query = Database.where('username', 'foo').into('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users" where "username" = ?')) + }) + + it('should be able to remove the prefix using the withoutPrefix method', function * () { + const query = Database.withoutPrefix().table('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "users"')) + }) + + it('should be able to remove the prefix when withoutPrefix method is called after other methods', function * () { + const query = Database.where('username', 'foo').withoutPrefix().table('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "users" where "username" = ?')) + }) + + it('should be able to change the prefix using the withPrefix method', function * () { + const query = Database.withPrefix('k_').table('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "k_users"')) + }) + + it('should be able to remove the prefix when withPrefix method is called after other methods', function * () { + const query = Database.where('username', 'foo').withPrefix('k_').table('users').toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "k_users" where "username" = ?')) + }) + + it('should not mess the query builder instance when withPrefix is called on multiple queries at same time', function * () { + const query = Database.where('username', 'foo').withPrefix('k_').table('users') + const query1 = Database.where('username', 'foo').table('users') + expect(queryHelpers.formatQuery(query.toSQL().sql)).to.equal(queryHelpers.formatQuery('select * from "k_users" where "username" = ?')) + expect(queryHelpers.formatQuery(query1.toSQL().sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users" where "username" = ?')) + }) + + it('should not mess the query builder instance when withoutPrefix is called on multiple queries at same time', function * () { + const query = Database.where('username', 'foo').withoutPrefix().table('users') + const query1 = Database.where('username', 'foo').table('users') + expect(queryHelpers.formatQuery(query.toSQL().sql)).to.equal(queryHelpers.formatQuery('select * from "users" where "username" = ?')) + expect(queryHelpers.formatQuery(query1.toSQL().sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users" where "username" = ?')) + }) }) diff --git a/test/unit/helpers/config.js b/test/unit/helpers/config.js index b21960fe..15abc48e 100644 --- a/test/unit/helpers/config.js +++ b/test/unit/helpers/config.js @@ -8,38 +8,47 @@ const mysqlConnections = require('./mysqlConnections') const postgresConnection = require('./postgresConnection') const sqliteConnections = require('./sqliteConnections') +const get = function (key, hasPrefix) { + if (key === 'database.migrationsTable') { + return 'adonis_migrations' + } -module.exports = { - get: function (key) { - if (key === 'database.migrationsTable') { - return 'adonis_migrations' - } - if (key === 'database.connection') { - return process.env.DB - } + if (key === 'database.connection') { + return process.env.DB + } - if (key === 'database.sqlite3') { - return sqliteConnections.default - } + if (key === 'database.sqlite3') { + return hasPrefix ? sqliteConnections.defaultPrefix : sqliteConnections.default + } - if (key === 'database.mysql') { - return mysqlConnections.default - } + if (key === 'database.mysql') { + return hasPrefix ? mysqlConnections.defaultPrefix : mysqlConnections.default + } - if (key === 'database.pg') { - return postgresConnection.default - } + if (key === 'database.pg') { + return hasPrefix ? postgresConnection.defaultPrefix : postgresConnection.default + } - if (key === 'database.alternateConnection' && process.env.DB === 'sqlite3') { - return sqliteConnections.alternateConnection - } + if (key === 'database.alternateConnection' && process.env.DB === 'sqlite3') { + return sqliteConnections.alternateConnection + } - if (key === 'database.alternateConnection' && process.env.DB === 'mysql') { - return mysqlConnections.alternateConnection - } + if (key === 'database.alternateConnection' && process.env.DB === 'mysql') { + return mysqlConnections.alternateConnection + } - if (key === 'database.alternateConnection' && process.env.DB === 'pg') { - return postgresConnection.alternateConnection + if (key === 'database.alternateConnection' && process.env.DB === 'pg') { + return postgresConnection.alternateConnection + } +} + +module.exports = { + get: function (key) { + return get(key, false) + }, + withPrefix: { + get: function (key) { + return get(key, true) } } } diff --git a/test/unit/helpers/mysqlConnections.js b/test/unit/helpers/mysqlConnections.js index d06943c8..f01a67d7 100644 --- a/test/unit/helpers/mysqlConnections.js +++ b/test/unit/helpers/mysqlConnections.js @@ -23,5 +23,15 @@ module.exports = { password : '', database : 'alternate' } + }, + + defaultPrefix : { + client: 'mysql', + connection: { + user : 'root', + password : '', + database : 'default' + }, + prefix: 'ad_' } } diff --git a/test/unit/helpers/postgresConnection.js b/test/unit/helpers/postgresConnection.js index efebf2d3..1e4dad76 100644 --- a/test/unit/helpers/postgresConnection.js +++ b/test/unit/helpers/postgresConnection.js @@ -23,5 +23,15 @@ module.exports = { password : '', database : 'alternate' } + }, + + defaultPrefix : { + client: 'pg', + connection: { + user: '', + password : '', + database : 'default' + }, + prefix: 'ad_' } } diff --git a/test/unit/helpers/sqliteConnections.js b/test/unit/helpers/sqliteConnections.js index a4ff49ba..fcf4f2ad 100644 --- a/test/unit/helpers/sqliteConnections.js +++ b/test/unit/helpers/sqliteConnections.js @@ -23,5 +23,15 @@ module.exports = { filename: path.join(__dirname, '../storage/test2.sqlite3') }, useNullAsDefault: true + }, + + defaultPrefix: { + client: 'sqlite3', + connection: { + filename: path.join(__dirname, '../storage/test.sqlite3') + }, + useNullAsDefault: true, + debug: false, + prefix: 'ad_' } } diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 71a03951..98fbde49 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -38,11 +38,19 @@ describe('Lucid', function () { }) after(function * () { + Database.close() + Database._setConfigProvider(config) yield modelFixtures.down(Database) Database.close() }) + beforeEach(function () { + Database.close() + }) + afterEach(function * () { + Database.close() + Database._setConfigProvider(config) yield Database.table('users').truncate() yield Database.table('zombies').truncate() }) @@ -124,6 +132,51 @@ describe('Lucid', function () { expect(User.globalScope[0]).to.be.a('function') expect(Post.globalScope.length).to.equal(1) }) + + it('should make use of the prefix when selecting the table @prefix', function () { + Database._setConfigProvider(config.withPrefix) + class User extends Model { + } + const query = User.query().toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "ad_users"')) + }) + + it('should be able to change the prefix for a single model when prefix getter is used', function () { + Database._setConfigProvider(config.withPrefix) + class User extends Model { + static get prefix () { + return 'k_' + } + } + const query = User.query().toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "k_users"')) + }) + + it('should be skip the prefix for a single model when skipPrefix getter is used', function () { + Database._setConfigProvider(config.withPrefix) + class User extends Model { + static get skipPrefix () { + return true + } + } + const query = User.query().toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "users"')) + }) + + it('should be skip the prefix for a single model when skipPrefix and prefix getter both are used', function () { + Database._setConfigProvider(config.withPrefix) + class User extends Model { + static get prefix () { + return 'k_' + } + + static get skipPrefix () { + return true + } + } + const query = User.query().toSQL() + expect(queryHelpers.formatQuery(query.sql)).to.equal(queryHelpers.formatQuery('select * from "users"')) + }) }) context('QueryBuilder', function () {