Skip to content

Commit

Permalink
Showing 15 changed files with 3,825 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage
node_modules
.DS_Store
npm-debug.log
.idea
out
.nyc_output
test/tmp
12 changes: 12 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
coverage
node_modules
.DS_Store
npm-debug.log
test
.travis.yml
.editorconfig
benchmarks
.idea
bin
out
.nyc_output
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js:
- node
- 7.0.0
sudo: false
install:
- npm install
notifications:
slack:
secure: m91zkX2cLVDRDMBAUnR1d+hbZqtSHXLkuPencHadhJ3C3wm53Box8U25co/goAmjnW5HNJ1SMSIg+DojtgDhqTbReSh5gSbU0uU8YaF8smbvmUv3b2Q8PRCA7f6hQiea+a8+jAb7BOvwh66dV4Al/1DJ2b4tCjPuVuxQ96Wll7Pnj1S7yW/Hb8fQlr9wc+INXUZOe8erFin+508r5h1L4Xv0N5ZmNw+Gqvn2kPJD8f/YBPpx0AeZdDssTL0IOcol1+cDtDzMw5PAkGnqwamtxhnsw+i8OW4avFt1GrRNlz3eci5Cb3NQGjHxJf+JIALvBeSqkOEFJIFGqwAXMctJ9q8/7XyXk7jVFUg5+0Z74HIkBwdtLwi/BTyXMZAgsnDjndmR9HsuBP7OSTJF5/V7HCJZAaO9shEgS8DwR78owv9Fr5er5m9IMI+EgSH3qtb8iuuQaPtflbk+cPD3nmYbDqmPwkSCXcXRfq3IxdcV9hkiaAw52AIqqhnAXJWZfL6+Ct32i2mtSaov9FYtp/G0xb4tjrUAsDUd/AGmMJNEBVoHtP7mKjrVQ35cEtFwJr/8SmZxGvOaJXPaLs43dhXKa2tAGl11wF02d+Rz1HhbOoq9pJvJuqkLAVvRdBHUJrB4/hnTta5B0W5pe3mIgLw3AmOpk+s/H4hAP4Hp0gOWlPA=
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Query Builder Features

- [x] Paginate method
- [x] forPage method
- [ ] chunk ( removed )
- [ ] pluckAll ( removed )
- [x] withPrefix
- [x] transactions
- [x] global transactions
22 changes: 22 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
environment:
matrix:
- nodejs_version: 'Stable'
- nodejs_version: '7'

init:
git config --global core.autocrlf true

install:
- ps: Install-Product node $env:nodejs_version
- npm install

test_script:
- node --version
- npm --version
- npm run test:win

build: off
clone_depth: 1

matrix:
fast_finish: true
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'use strict'
3,097 changes: 3,097 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@adonisjs/lucid",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/adonisjs/adonis-lucid.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/adonisjs/adonis-lucid/issues"
},
"homepage": "https://github.com/adonisjs/adonis-lucid#readme",
"dependencies": {
"knex": "^0.13.0",
"lodash": "^4.17.4",
"node-exceptions": "^2.0.2"
},
"devDependencies": {
"@adonisjs/sink": "^1.0.8",
"chance": "^1.0.9",
"coveralls": "^2.13.1",
"fs-extra": "^3.0.1",
"japa": "^1.0.3",
"japa-cli": "^1.0.1",
"mysql": "^2.13.0",
"nyc": "^11.0.2",
"sqlite3": "^3.1.8"
}
}
66 changes: 66 additions & 0 deletions src/Database/Manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict'

/*
* adonis-lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

require('./MonkeyPatch')

const Database = require('.')
const CE = require('../Exceptions')

const proxyHandler = {
get (target, name) {
if (target[name]) {
return target[name]
}

const db = target.connection()
if (typeof (db[name]) === 'function') {
return db[name].bind(db)
}
return db[name]
}
}

/**
* DatabaseManager is a layer on top of {{crossLink "Database"}}{{/crossLink}}
* class which manages a pool of different database connections and proxy
* all Database methods, so that it's easier to work with them.
*/
class DatabaseManager {
constructor (Config) {
this.Config = Config
this._connectionPools = {}
return new Proxy(this, proxyHandler)
}

/**
* Creates a new/resuse and returns the database connection.
*
* @method connection
*
* @param {String} [name = Config.get('database.connection')]
*
* @return {Database}
*/
connection (name = this.Config.get('database.connection')) {
if (this._connectionPools[name]) {
return this._connectionPools[name]
}
const connectionSettings = this.Config.get(`database.${name}`)
if (!connectionSettings) {
throw CE.RuntimeException.missingDatabaseConnection(name)
}

this._connectionPools[name] = new Database(connectionSettings)
return this._connectionPools[name]
}
}

module.exports = DatabaseManager
69 changes: 69 additions & 0 deletions src/Database/MonkeyPatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict'

/*
* adonis-lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* Here we monkey patch/extend knex query builder
* prototype.
*/

const _ = require('lodash')
var KnexQueryBuilder = require('knex/lib/query/builder')
const excludeAttrFromCount = ['order']

const _from = KnexQueryBuilder.prototype.from

KnexQueryBuilder.prototype.from = function (name) {
const prefix = _.get(this.client, 'config.prefix')
name = prefix && !this._ignorePrefix ? `${prefix}${name}` : name
return _from.bind(this)(name)
}

KnexQueryBuilder.prototype.table = function (...args) {
return this.from(...args)
}

KnexQueryBuilder.prototype.into = function (...args) {
return this.from(...args)
}

KnexQueryBuilder.prototype.withOutPrefix = function () {
this._ignorePrefix = true
return this
}

KnexQueryBuilder.prototype.forPage = function (page, perPage = 20) {
const offset = page === 1 ? 0 : perPage * (page - 1)
return this.offset(offset).limit(perPage)
}

KnexQueryBuilder.prototype.paginate = async function (page, perPage = 20) {
const countByQuery = this.clone()

/**
* Remove statements that will make things bad with count
* query, for example `orderBy`
*/
countByQuery._statements = _.filter(countByQuery._statements, (statement) => {
return excludeAttrFromCount.indexOf(statement.grouping) < 0
})

const counts = await countByQuery.count('* as total')
const total = _.get(counts, '0.total', 0)
const data = total === 0 ? [] : await this.forPage(page, perPage)

return {
total: total,
perPage: perPage,
page: page,
lastPage: Math.ceil(total / perPage),
data: data
}
}
155 changes: 155 additions & 0 deletions src/Database/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use strict'

/*
* adonis-lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

const knex = require('knex')
const _ = require('lodash')

const proxyHandler = {
get (target, name) {
if (typeof (target[name]) !== 'undefined') {
return target[name]
}

const queryBuilder = target.query()
if (typeof (queryBuilder[name]) !== 'function') {
throw new Error(`Database.${name} is not a function`)
}

if (target._globalTrx) {
queryBuilder.transacting(target._globalTrx)
}
return queryBuilder[name].bind(queryBuilder)
}
}

/**
* Database class instance is used to initiate database
* queries and transactions.
*
* @class Database
* @constructor
*/
class Database {
constructor (config) {
if (config.client === 'sqlite') {
config.useNullAsDefault = _.defaultTo(config.useNullAsDefault, true)
}
this.knex = knex(config)
this._globalTrx = null
return new Proxy(this, proxyHandler)
}

/**
* Returns the schema builder
*
* @attribute schema
*
* @return {Object}
*/
get schema () {
return this.knex.schema
}

/**
* Method to build raw queries
*
* @method raw
*
* @param {...Spread} args
*
* @return {String}
*/
raw (...args) {
return this.knex.raw(...args)
}

/**
* Returns a trx object to be used for running queries
* under transaction.
*
* @method beginTransaction
*
* @return {Promise}
*/
beginTransaction () {
return new Promise((resolve, reject) => {
this
.knex
.transaction(function (trx) {
resolve(trx)
}).catch(() => {})
})
}

/**
* Starts a global transaction, where all query builder
* methods will be part of transaction automatically.
*
* Note: You must not use it in real world apart from when
* writing tests.
*
* @method beginGlobalTransaction
*
* @return {void}
*/
async beginGlobalTransaction () {
this._globalTrx = await this.beginTransaction()
}

/**
* Rollbacks global transaction
*
* @method rollbackGlobalTransaction
*
* @return {void}
*/
rollbackGlobalTransaction () {
this._globalTrx.rollback()
this._globalTrx = null
}

/**
* Commits global transaction
*
* @method commitGlobalTransaction
*
* @return {void}
*/
commitGlobalTransaction () {
this._globalTrx.commit()
this._globalTrx = null
}

/**
* Return a new instance of query builder
*
* @method query
*
* @return {Object}
*/
query () {
return new this.knex.queryBuilder()
}

/**
* Closes the database connection. No more queries
* can be made after this
*
* @method close
*
* @return {Promise}
*/
close () {
return this.knex.destroy()
}
}

module.exports = Database
30 changes: 30 additions & 0 deletions src/Exceptions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict'

/*
* adonis-lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

const NE = require('node-exceptions')

class RuntimeException extends NE.RuntimeException {
/**
* This exception is raised when user is trying to use an
* undefined database connection
*
* @method missingDatabaseConnection
*
* @param {String} name
*
* @return {Object}
*/
static missingDatabaseConnection (name) {
return new this(`Missing database connection {${name}}. Make sure you define it inside config/database.js file`, 500, 'E_MISSING_DB_CONNECTION')
}
}

module.exports = { RuntimeException }
243 changes: 243 additions & 0 deletions test/unit/database.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
'use strict'

/*
* adonis-lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

const test = require('japa')
const chance = require('chance').Chance()
const _ = require('lodash')
const fs = require('fs-extra')
const path = require('path')
const { Config } = require('@adonisjs/sink')
const Database = require('../../src/Database')
const DatabaseManager = require('../../src/Database/Manager')
const helpers = require('./helpers')

test.group('Database | QueryBuilder', (group) => {
group.before(async () => {
await fs.ensureDir(path.join(__dirname, './tmp'))
this.database = new Database(helpers.getConfig())
await helpers.createTables(this.database)
})

group.after(async () => {
await helpers.dropTables(this.database)
await fs.remove(path.join(__dirname, './tmp'))
})

test('get instance of query builder', (assert) => {
const queryBuilder = this.database.query()
assert.property(queryBuilder, 'client')
})

test('proxy query builder methods', (assert) => {
const selectQuery = this.database.from('users').toSQL()
assert.equal(selectQuery.sql, helpers.formatQuery('select * from "users"'))
})

test('prefix table when defined inside config', (assert) => {
const dbConfig = helpers.getConfig()
dbConfig.prefix = 'my_'

const selectQuery = new Database(dbConfig).from('users').toSQL()
assert.equal(selectQuery.sql, helpers.formatQuery('select * from "my_users"'))
})

test('ignore prefix at runtime', (assert) => {
const dbConfig = helpers.getConfig()
dbConfig.prefix = 'my_'

const selectQuery = new Database(dbConfig).withOutPrefix().from('users').toSQL()
assert.equal(selectQuery.sql, helpers.formatQuery('select * from "users"'))
})

test('create a raw query', (assert) => {
const selectQuery = this.database.raw(helpers.formatQuery('select * from "users"'))
assert.equal(selectQuery.sql, helpers.formatQuery('select * from "users"'))
})

test('create db transactions', async (assert) => {
const trx = await this.database.beginTransaction()
assert.isDefined(trx)
trx.rollback()
})

test('commit transactions', async (assert) => {
const trx = await this.database.beginTransaction()
await trx.table('users').insert({ username: 'virk' })
trx.commit()
const firstUser = await this.database.table('users').first()
assert.equal(firstUser.username, 'virk')
await this.database.truncate('users')
})

test('rollback transactions', async (assert) => {
const trx = await this.database.beginTransaction()
await trx.table('users').insert({ username: 'virk' })
trx.rollback()
const users = await this.database.table('users')
assert.lengthOf(users, 0)
})

test('transaction should not collide with other queries', async (assert) => {
const trx = await this.database.beginTransaction()
setTimeout(() => {
trx.rollback()
}, 20)
await this.database.table('users').insert({ username: 'virk' })
const users = await this.database.table('users')
assert.lengthOf(users, 1)
await this.database.truncate('users')
})

test('create global transactions', async (assert) => {
await this.database.beginGlobalTransaction()
await this.database.table('users').insert({ username: 'virk' })
this.database.rollbackGlobalTransaction()
const users = await this.database.table('users')
assert.lengthOf(users, 0)
})

test('commit global transactions', async (assert) => {
await this.database.beginGlobalTransaction()
await this.database.table('users').insert({ username: 'virk' })
this.database.commitGlobalTransaction()
const users = await this.database.table('users')
assert.lengthOf(users, 1)
await this.database.truncate('users')
})

test('destroy database connection', async (assert) => {
await this.database.close()
assert.plan(1)
try {
await this.database.table('users')
} catch ({ message }) {
assert.equal(message, 'Unable to acquire a connection')
this.database = new Database(helpers.getConfig())
}
})

test('add orderBy and limit clause using forPage method', async (assert) => {
const query = this.database.table('users').forPage(1).toSQL()
assert.equal(query.sql, helpers.formatQuery('select * from "users" limit ?'))
assert.deepEqual(query.bindings, [20])
})

test('add orderBy and limit clause using forPage greater than 1', async (assert) => {
const query = this.database.table('users').forPage(3).toSQL()
assert.equal(query.sql, helpers.formatQuery('select * from "users" limit ? offset ?'))
assert.deepEqual(query.bindings, [20, 40])
await this.database.table('users').truncate()
})

test('paginate results', async (assert) => {
const users = _.map(_.range(10), () => {
return { username: chance.word() }
})
await this.database.insert(users).into('users')
const result = await this.database.table('users').orderBy('username').paginate(1, 5)
assert.equal(result.perPage, 5)
assert.equal(result.total, 10)
assert.equal(result.page, 1)
assert.equal(result.lastPage, 2)
assert.isAtMost(result.data.length, result.perPage)
await this.database.table('users').truncate()
})

test('paginate results when records are less than perPage', async (assert) => {
const users = _.map(_.range(4), () => {
return { username: chance.word() }
})
await this.database.insert(users).into('users')
const result = await this.database.table('users').orderBy('username').paginate(1, 5)
assert.equal(result.perPage, 5)
assert.equal(result.total, 4)
assert.equal(result.page, 1)
assert.equal(result.lastPage, 1)
assert.isAtMost(result.data.length, result.perPage)
await this.database.table('users').truncate()
})

test('paginate data inside transactions', async (assert) => {
const trx = await this.database.beginTransaction()
assert.equal(typeof(trx.table('users').paginate), 'function')
trx.rollback()
})

test('throw exception when proxy property is not a method', (assert) => {
const fn = () => this.database.foo()
assert.throw(fn, 'Database.foo is not a function')
})
})

test.group('Database | Manager', () => {
test('get instance of database using connection method', (assert) => {
const config = new Config()
config.set('database', {
connection: 'testing',
testing: helpers.getConfig()
})
const db = new DatabaseManager(config).connection()
assert.instanceOf(db, Database)
})

test('throw exception when unable to connect to database', (assert) => {
const config = new Config()
config.set('database', {
connection: 'testing',
testing: {}
})
const db = () => new DatabaseManager(config).connection()
assert.throw(db, 'knex: Required configuration option \'client\' is missing')
})

test('throw exception when connection does not exists', (assert) => {
const config = new Config()
config.set('database', {
connection: 'testing',
testing: {}
})
const db = () => new DatabaseManager(config).connection('foo')
assert.throw(db, 'E_MISSING_DB_CONNECTION: Missing database connection {foo}. Make sure you define it inside config/database.js file')
})

test('proxy database methods', (assert) => {
const config = new Config()
config.set('database', {
connection: 'testing',
testing: helpers.getConfig()
})
const query = new DatabaseManager(config).table('users').toSQL()
assert.equal(query.sql, helpers.formatQuery('select * from "users"'))
})

test('proxy database properties', (assert) => {
const config = new Config()
config.set('database', {
connection: 'testing',
testing: helpers.getConfig()
})
assert.isNull(new DatabaseManager(config)._globalTrx)
})

test('reuse existing database connection', (assert) => {
const config = new Config()
config.set('database', {
connection: 'testing',
testing: helpers.getConfig()
})
const dbManager = new DatabaseManager(config)
dbManager.connection()
assert.lengthOf(Object.keys(dbManager._connectionPools), 1)

dbManager.connection()
assert.lengthOf(Object.keys(dbManager._connectionPools), 1)
})
})
55 changes: 55 additions & 0 deletions test/unit/helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict'

const path = require('path')
const _ = require('lodash')

module.exports = {
formatQuery (query, connection) {
if (process.env.DB === 'sqlite') {
return query
}

if (process.env.DB === 'mysql') {
return query.replace(/"/g, '`')
}
},

getConfig () {
if (process.env.DB === 'sqlite') {
return _.cloneDeep({
client: 'sqlite',
connection: {
filename: path.join(__dirname, '../tmp/dev.sqlite3')
}
})
}

if (process.env.DB === 'mysql') {
return _.cloneDeep({
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'root',
password: '',
database: 'testing_lucid'
}
})
}
},

createTables (db) {
return Promise.all([
db.schema.createTable('users', function (table) {
table.increments()
table.string('username')
table.timestamps()
})
])
},

dropTables (db) {
return Promise.all([
db.schema.dropTable('users')
])
}
}

0 comments on commit 00f091d

Please sign in to comment.