From 4eda47c8b58feb160d98fb5ea830eaec44b4ad3d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 16 Feb 2016 20:05:32 +0530 Subject: [PATCH] feat(*): first draft implemented database provider --- .editorconfig | 13 + .gitignore | 4 + .npmignore | 8 + .travis.yml | 18 ++ CONTRIBUTING.md | 6 + README.md | 56 ++++ lib/util.js | 202 ++++++++++++++ package.json | 40 +++ src/Database/index.js | 348 +++++++++++++++++++++++++ test/unit/database.spec.js | 256 ++++++++++++++++++ test/unit/fixtures/files.js | 20 ++ test/unit/fixtures/model.js | 85 ++++++ test/unit/fixtures/relations.js | 77 ++++++ test/unit/helpers/config.js | 34 +++ test/unit/helpers/mysqlConnections.js | 27 ++ test/unit/helpers/query.js | 16 ++ test/unit/helpers/sqliteConnections.js | 27 ++ test/unit/storage/test.sqlite3 | Bin 0 -> 9216 bytes test/unit/storage/test2.sqlite3 | Bin 0 -> 9216 bytes test/unit/util.spec.js | 115 ++++++++ 20 files changed, 1352 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 lib/util.js create mode 100644 package.json create mode 100644 src/Database/index.js create mode 100644 test/unit/database.spec.js create mode 100644 test/unit/fixtures/files.js create mode 100644 test/unit/fixtures/model.js create mode 100644 test/unit/fixtures/relations.js create mode 100644 test/unit/helpers/config.js create mode 100644 test/unit/helpers/mysqlConnections.js create mode 100644 test/unit/helpers/query.js create mode 100644 test/unit/helpers/sqliteConnections.js create mode 100644 test/unit/storage/test.sqlite3 create mode 100644 test/unit/storage/test2.sqlite3 create mode 100644 test/unit/util.spec.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..91422397 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..66bc7212 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +coverage +node_modules +.DS_Store +npm-debug.log diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..293348e0 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +coverage +node_modules +.DS_Store +npm-debug.log +test +.travis.yml +.editorconfig +benchmarks diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b7eba7b4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: node_js +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 +node_js: +- 5.3.0 +- 4.0.0 +sudo: false +install: +- npm install --no-optional +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= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..256a12e0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +In favor of active development we accept contributions from everyone. You can contribute by submitting a bug, creating pull requests or even by improving documentation. + +Below is the guide to be followed strictly before submitting your pull requests. +http://adonisjs.com/docs/2.0/contributing diff --git a/README.md b/README.md new file mode 100644 index 00000000..92a9c24f --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# AdonisJS Framework + +[![Gitter](https://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square)](https://gitter.im/adonisjs/adonis-framework) +[![Trello](https://img.shields.io/badge/TRELLO-%E2%86%92-89609E.svg?style=flat-square)](https://trello.com/b/yzpqCgdl/adonis-for-humans) +[![Version](https://img.shields.io/npm/v/adonis-framework.svg?style=flat-square)](https://www.npmjs.com/package/adonis-framework) +[![Build Status](https://img.shields.io/travis/adonisjs/adonis-framework/master.svg?style=flat-square)](https://travis-ci.org/adonisjs/adonis-framework) +[![Coverage Status](https://img.shields.io/coveralls/adonisjs/adonis-framework/master.svg?style=flat-square)](https://coveralls.io/github/adonisjs/adonis-framework?branch=master) +[![Downloads](https://img.shields.io/npm/dt/adonis-framework.svg?style=flat-square)](https://www.npmjs.com/package/adonis-framework) +[![License](https://img.shields.io/npm/l/adonis-framework.svg?style=flat-square)](https://opensource.org/licenses/MIT) + +> :pray: This repository contains the core of the AdonisJS framework. + +Adonis is a MVC framework for NodeJS built on solid foundations. + +It is the first NodeJS framework with support for [Dependency Injection](http://adonisjs.com/docs/2.0/dependency-injection) and has a lean [IoC Container](http://adonisjs.com/docs/2.0/ioc-container) to resolve and mock dependencies. It borrows the concept of [Service Providers](http://adonisjs.com/docs/2.0/service-providers) from the popular [PHP framework Laravel](https://laravel.com) to write scalable applications. + +You can learn more about AdonisJS and all of its awesomeness on http://adonisjs.com :evergreen_tree: + +## Table of Contents + +* [Team Members](#team-members) +* [Requirements](#requirements) +* [Getting Started](#getting-started) +* [Contribution Guidelines](#contribution-guidelines) + +## Team Members + +* Harminder Virk ([Caffiene Blogging](http://amanvirk.me/)) + +## Requirements + +AdonisJS is build on the top of ES2015, which makes the code more enjoyable and cleaner to read. It doesn't make use of any transpiler and depends upon Core V8 implemented features. + +For these reasons, AdonisJS require you to use `node >= 4.0` and `npm >= 3.0`. + +## Getting Started + +AdonisJS provide a [CLI tool](https://github.com/AdonisJs/adonis-cli) to scaffold and generate a project with all required dependencies. + +```bash +$ npm install -g adonis-cli +``` + +```bash +$ adonis new awesome-project +$ cd awesome-project +$ npm run start +``` + +[Official Documentation](http://adonisjs.com/docs/2.0/installation) + +## Contribution Guidelines + +In favor of active development we accept contributions for everyone. You can contribute by submitting a bug, creating pull requests or even improving documentation. + +You can find a complete guide to be followed strictly before submitting your pull requests in the [Official Documentation](http://adonisjs.com/docs/2.0/contributing). diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 00000000..df8cbf46 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,202 @@ +'use strict' + +/** + * adonis-framework + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +const i = require('inflect') +const _ = require('lodash') +const util = exports = module.exports = {} + +/** + * makes table name from a class defination + * + * @method makeTableName + * + * @param {Object} Model + * @return {String} + * + * @public + */ +util.makeTableName = function (Model) { + let modelName = Model.name + modelName = i.pluralize(modelName) + return i.underscore(modelName) +} + +/** + * makes a foreign key from a class + * defination + * + * @method makeForeignKey + * @param {Object} Model + * @return {String} + * + * @public + */ +util.makeForeignKey = function (Model) { + let modelName = Model.name + modelName = i.singularize(modelName) + return `${i.underscore(modelName)}_id` +} + +/** + * omits key/values pairs from an objec + * @method omit + * + * @param {Object} values + * @param { valuesToOmit + * @return {Array|Object} + * + * @public + */ +util.omit = function (values, valuesToOmit) { + return _.omit(values, valuesToOmit) +} + +/** + * pick key/values pairs from an object + * + * @method pick + * @param {Object} values + * @param {Array} valuesToOmit + * @return {Object} + * + * @public + */ +util.pick = function (values, valuesToOmit) { + return _.pick(values, valuesToOmit) +} + +/** + * wraps an object into lodash collection + * + * @method toCollection + * @param {Array|Object} values + * @return {Array|Object} + * + * @public + */ +util.toCollection = function (values) { + return _(values) +} + +/** + * makes a getter name for a given field + * + * @method makeGetterName + * @param {String} name + * @return {String} + * + * @public + */ +util.makeGetterName = function (name) { + return `get${i.camelize(i.underscore(name))}` +} + +/** + * makes a getter name for a given field + * + * @method makeSetterName + * @param {String} name + * @return {String} + * + * @public + */ +util.makeSetterName = function (name) { + return `set${i.camelize(i.underscore(name))}` +} + +/** + * map values for a given key and returns + * the transformed array with that key only + * + * @method mapValuesForAKey + * @param {Array} values + * @param {String} key + * @return {Array} + * + * @public + */ +util.mapValuesForAKey = function (values, key) { + return _.map(values, function (value) { + return value[key] + }) +} + +/** + * calculates offset for a given page using + * page and perPage options + * + * @method returnOffset + * @param {Number} page + * @param {Number} perPage + * @return {Number} + * + * @public + */ +util.returnOffset = function (page, perPage) { + return page === 1 ? 0 : ((perPage * (page - 1))) +} + +/** + * validates a page to be a number and greater + * than 0. this is something required to paginate results. + * + * @method validatePage + * @param {Number} page + * @return {void} + * @throws {Error} If page is not a number of less than 1 + * + * @public + */ +util.validatePage = function (page) { + if (typeof (page) !== 'number') { + throw new Error('page parameter is required to paginate results') + } + if (page < 1) { + throw new Error('cannot paginate results for page less than 1') + } +} + +/** + * make meta data for paginated results. + * + * @method makePaginateMeta + * + * @param {Number} total + * @param {Number} page + * @param {Number} perPage + * @return {Object} + * + * @public + */ +util.makePaginateMeta = function (total, page, perPage) { + const resultSet = { + total: total, + currentPage: page, + perPage: perPage, + lastPage: 0, + data: [] + } + if (total > 0) { + resultSet.lastPage = Math.ceil(total / perPage) + } + return resultSet +} + +/** + * capitalizes a given string + * + * @method capitalize + * @param {String} value + * @return {String} + * + * @public + */ +util.capitalize = i.capitalize diff --git a/package.json b/package.json new file mode 100644 index 00000000..176a8bf1 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "adonis-lucid", + "version": "3.0.0", + "description": "[![Gitter](https://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square)](https://gitter.im/adonisjs/adonis-framework) [![Trello](https://img.shields.io/badge/TRELLO-%E2%86%92-89609E.svg?style=flat-square)](https://trello.com/b/yzpqCgdl/adonis-for-humans) [![Version](https://img.shields.io/npm/v/adonis-framework.svg?style=flat-square)](https://www.npmjs.com/package/adonis-framework) [![Build Status](https://img.shields.io/travis/adonisjs/adonis-framework/master.svg?style=flat-square)](https://travis-ci.org/adonisjs/adonis-framework) [![Coverage Status](https://img.shields.io/coveralls/adonisjs/adonis-framework/master.svg?style=flat-square)](https://coveralls.io/github/adonisjs/adonis-framework?branch=master) [![Downloads](https://img.shields.io/npm/dt/adonis-framework.svg?style=flat-square)](https://www.npmjs.com/package/adonis-framework) [![License](https://img.shields.io/npm/l/adonis-framework.svg?style=flat-square)](https://opensource.org/licenses/MIT)", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "npm run standard && node --harmony_proxies ./node_modules/.bin/_mocha test --recursive", + "standard": "standard src/**/*.js lib/*.js test/**/*.js" + }, + "author": "adonisjs", + "license": "MIT", + "devDependencies": { + "bluebird": "^3.3.1", + "chai": "^3.5.0", + "co-fs-extra": "^1.1.0", + "co-mocha": "^1.1.2", + "coveralls": "^2.11.6", + "cz-conventional-changelog": "^1.1.5", + "istanbul": "^0.4.2", + "mocha": "^2.4.5", + "mysql": "^2.10.2", + "sqlite3": "^3.1.1", + "standard": "^6.0.5" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "dependencies": { + "co": "^4.6.0", + "harmony-reflect": "^1.4.2", + "inflect": "^0.3.0", + "knex": "^0.10.0", + "lodash": "^4.3.0" + } +} diff --git a/src/Database/index.js b/src/Database/index.js new file mode 100644 index 00000000..5382f8c3 --- /dev/null +++ b/src/Database/index.js @@ -0,0 +1,348 @@ +'use strict' + +/** + * adonis-framework + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +/** + * to have stable support for proxies + * we need harmony-reflect + */ +require('harmony-reflect') + +const knex = require('knex') +const util = require('../../lib/util') +const co = require('co') + +/** + * here we store connection pools, created by database + * provider. It is required as one can use multiple connections + * using database provider.And spwaning a connection everytime + * will blow up things. + * + * @type {Object} + * + * @private + */ +let connectionPools = {} + +/** + * reference to config provider, since + * we do not require providers and instead get + * them from IOC container. + * + * @type {Object} + * + * @private + */ +let ConfigProvider = {} + +/** + * Database provider to build sql queries + * @module Database + */ +const Database = {} +/** + * sets the config provider for database module + * @method _setConfigProvider. It is set while registering + * it inside the IOC container. So no one will ever to + * deal with it manaully. + * + * @param {Object} Config + * + * @private + */ +Database._setConfigProvider = function (Config) { + ConfigProvider = Config +} + +/** + * returns configuration for a given connection name + * + * @method getConfig + * @param {String} connection + * @return {Object} + * + * @private + */ +Database._getConfig = function (connection) { + if (connection === 'default') { + connection = ConfigProvider.get('database.connection') + if (!connection) { + throw new Error('connection is not defined inside database config file') + } + } + return ConfigProvider.get(`database.${connection}`) +} + +/** + * returns knex instance for a given connection + * if it exists it's fine, otherwise a pool is created and + * returned + * + * @method getConnection + * @param {String} connection + * @return {Object} + * + * @private + */ +Database._getConnection = function (connection) { + if (!connectionPools[connection]) { + const config = Database._getConfig(connection) + if (!config) { + throw new Error(`Unable to get database client configuration using ${connection} key`) + } + const client = knex(config) + const rawTransaction = client.transaction + client.transaction = Database.transaction(rawTransaction) + client.beginTransaction = Database.beginTransaction(rawTransaction) + client.client.QueryBuilder.prototype.forPage = Database.forPage + client.client.QueryBuilder.prototype.paginate = Database.paginate + client.client.QueryBuilder.prototype.chunk = Database.chunk + connectionPools[connection] = client + } + return connectionPools[connection] +} + +/** + * creates a connection pool for a given connection + * if does not exists and returns the knex instance + * for a given connection defined inside database + * config file. + * + * @method connection + * @param {String} connection Name of the connection to return pool + * instance for + * @return {Object} + * + * @example + * Database.connection('mysql') + * Database.connection('sqlite') + * + * @public + */ +Database.connection = function (connection) { + return Database._getConnection(connection) +} + +/** + * returns list of connection pools created + * so far + * + * @method getConnectionPools + * + * @return {Object} + * @public + */ +Database.getConnectionPools = function () { + return connectionPools +} + +/** + * closes database connection by destroying the client + * and remove it from the pool. + * + * @method close + * + * @param {String} [connection] connection name to close, if not provided all + * connections will get closed. + * @return {void} + * + * @public + */ +Database.close = function (connection) { + if (connection && connectionPools[connection]) { + connectionPools[connection].client.destroy() + delete connectionPools[connection] + return + } + + const poolKeys = Object.keys(connectionPools) + poolKeys.forEach(function (key) { + connectionPools[key].client.destroy() + }) + connectionPools = {} +} + +/** + * beginTransaction is used for doing manual commit + * and rollbacks. Errors emitted from this method are voided. + * + * @method beginTransaction + * + * @param {Function} clientTransaction original transaction method from knex instance + * @return {Function} + * + * @example + * const trx = yield Database.beginTransaction() + * yield Database.table('users').transacting(trx) + * trx.commit() + * trx.rollback() + * + * @public + */ +Database.beginTransaction = function (clientTransaction) { + return function () { + return new Promise(function (resolve, reject) { + clientTransaction(function (trx) { + resolve(trx) + }) + .catch(function () { + /** + * adding a dummy handler to avoid exceptions from getting thrown + * as this method does not need a handler + */ + }) + }) + } +} + +/** + * overrides the actual transaction method on knex + * to have a transaction method with support for + * generator methods + * @method transaction + * @param {Function} clientTransaction original transaction method from knex instance + * @return {Function} + * + * @example + * Database.transaction(function * (trx) { + * yield trx.table('users') + * }) + * + * @public + */ +Database.transaction = function (clientTransaction) { + return function (cb) { + return clientTransaction(function (trx) { + co(function * () { + return yield cb(trx) + }) + .then(trx.commit) + .catch(trx.rollback) + }) + } +} + +/** + * sets offset and limit on query chain using + * current page and perpage params + * + * @method forPage + * + * @param {Number} page + * @param {Number} [perPage=20] + * @return {Object} + * + * @example + * Database.table('users').forPage(1) + * Database.table('users').forPage(1, 30) + * + * @public + */ +Database.forPage = function (page, perPage) { + util.validatePage(page) + perPage = perPage || 20 + const offset = util.returnOffset(page, perPage) + return this.offset(offset).limit(perPage) +} + +/** + * gives paginated results for a given + * query. + * + * @method paginate + * + * @param {Number} page + * @param {Number} [perPage=20] + * @return {Array} + * + * @example + * Database.table('users').paginate(1) + * Database.table('users').paginate(1, 30) + * + * @public + */ +Database.paginate = function * (page, perPage) { + perPage = perPage || 20 + util.validatePage(page) + /** + * first we count the total rows before making the actual + * actual for getting results + */ + const queryClone = this.clone() + const count = yield queryClone.count('* as total') + if (!count[0] || parseInt(count[0].total, 10) === 0) { + return util.makePaginateMeta(0, page, perPage) + } + + /** + * here we fetch results and set meta data for paginated + * results + */ + const results = yield this.forPage(page, perPage) + const resultSet = util.makePaginateMeta(parseInt(count[0].total, 10), page, perPage) + resultSet.data = results + return resultSet +} + +/** + * returns chunk of data under a defined limit of results, and + * invokes a callback, everytime there are results. + * + * @method *chunk + * + * @param {Number} limit + * @param {Function} cb + * @param {Number} [page=1] + * + * @example + * Database.table('users').chunk(200, function (users) { + * + * }) + * + * @public + */ +Database.chunk = function * (limit, cb, page) { + page = page || 1 + const result = yield this.forPage(page, limit) + if (result.length) { + cb(result) + page++ + yield this.chunk(limit, cb, page) + } +} + +/** + * these methods are not proxied and instead actual implementations + * are returned + * + * @type {Array} + * + * @private + */ +const methodsNotToProxy = ['_getConfig', '_setConfigProvider', 'getConnectionPools', 'connection', 'close'] + +/** + * Proxy handler to proxy methods and send + * them to knex directly. + * + * @type {Object} + * + * @private + */ +const DatabaseProxy = { + get: function (target, method) { + if (methodsNotToProxy.indexOf(method) > -1) { + return target[method] + } + return Database._getConnection('default')[method] + } +} + +module.exports = new Proxy(Database, DatabaseProxy) diff --git a/test/unit/database.spec.js b/test/unit/database.spec.js new file mode 100644 index 00000000..4c3db08b --- /dev/null +++ b/test/unit/database.spec.js @@ -0,0 +1,256 @@ +'use strict' + +/** + * adonis-framework + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +/* global describe, it, beforeEach, after, before */ +const Database = require('../../src/Database') +const chai = require('chai') +const filesFixtures = require('./fixtures/files') +const modelFixtures = require('./fixtures/model') +const config = require('./helpers/config') +const queryHelpers = require('./helpers/query') +const expect = chai.expect +require('co-mocha') + +describe('Database provider', function () { + beforeEach(function () { + Database.close() + }) + + before(function * () { + Database._setConfigProvider(config) + yield filesFixtures.createDir() + yield modelFixtures.up(Database) + }) + + after(function * () { + yield modelFixtures.down(Database) + yield modelFixtures.down(Database.connection('alternateConnection')) + Database.close() + }) + + it('should set config provider', function () { + Database._setConfigProvider(config) + const settings = Database._getConfig('sqlite3') + expect(settings.client).to.equal('sqlite3') + }) + + it('should setup a knex instance of default connection', function () { + Database._setConfigProvider(config) + const instance = Database.table('users') + expect(instance.client.config.client).to.equal(process.env.DB) + }) + + it('should throw an error when unable to find connection property on config object', function () { + const Config = { + get: function () { + return null + } + } + Database._setConfigProvider(Config) + const fn = function () { + Database.where() + } + expect(fn).to.throw(/connection is not defined inside database config file/) + }) + + it('should throw an error when unable to find connection settings using connection key', function () { + const Config = { + get: function (key) { + if (key === 'database.connection') { + return 'sqlite' + } + return null + } + } + Database._setConfigProvider(Config) + const fn = function () { + Database.where() + } + expect(fn).to.throw(/Unable to get database client configuration using default key/) + }) + + it('should reuse the old pool if exists', function () { + Database._setConfigProvider(config) + Database.table('users') + Database.table('accounts') + const pools = Database.getConnectionPools() + expect(Object.keys(pools).length).to.equal(1) + }) + + it('should be able to chain all knex methods', function () { + Database._setConfigProvider(config) + const sql = Database.table('users').where('username', 'bar').toSQL().sql + expect(sql).to.equal(queryHelpers.formatQuery('select * from "users" where "username" = ?')) + }) + + it('should not use global scope for query chain', function () { + Database._setConfigProvider(config) + const user = Database.table('users').where('username', 'foo') + const accounts = Database.table('accounts').where('id', 1) + expect(user.toSQL().sql).to.equal(queryHelpers.formatQuery('select * from "users" where "username" = ?')) + expect(accounts.toSQL().sql).to.equal(queryHelpers.formatQuery('select * from "accounts" where "id" = ?')) + }) + + it('should spawn a new connection pool when connection method is used', function () { + Database._setConfigProvider(config) + Database.select() + const instance = Database.connection('alternateConnection') + expect(Object.keys(Database.getConnectionPools()).length).to.equal(2) + expect(instance.client.config.client).not.equal(undefined) + }) + + it('should be able to chain query incrementally', function () { + Database._setConfigProvider(config) + const user = Database.table('users') + user.where('age', 22) + user.where('username', 'virk') + expect(user.toSQL().sql).to.equal(queryHelpers.formatQuery('select * from "users" where "age" = ? and "username" = ?')) + }) + + it('should close a given connection', function () { + Database._setConfigProvider(config) + Database.table('users') + Database.connection('alternateConnection') + Database.close('default') + expect(Object.keys(Database.getConnectionPools()).length).to.equal(1) + }) + + it('should close all connection', function () { + Database._setConfigProvider(config) + Database.table('users') + Database.connection('alternateConnection') + Database.close() + expect(Object.keys(Database.getConnectionPools()).length).to.equal(0) + }) + + it('should be able to create lean transactions', function * () { + const trx = yield Database.beginTransaction() + yield trx.table('users').insert({username: 'db-trx'}) + trx.commit() + const user = yield Database.table('users').where('username', 'db-trx') + expect(user).to.be.an('array') + expect(user[0].username).to.equal('db-trx') + }) + + it('should be able to rollback transactions', function * () { + const trx = yield Database.beginTransaction() + yield trx.table('users').insert({username: 'db-trx1'}) + trx.rollback() + const user = yield Database.table('users').where('username', 'db-trx1') + expect(user).to.be.an('array') + expect(user.length).to.equal(0) + }) + + it('should be able to have multiple transactions', function * () { + const trx = yield Database.beginTransaction() + yield trx.table('users').insert({username: 'multi-trx'}) + trx.commit() + + const trx1 = yield Database.beginTransaction() + yield trx1.table('users').insert({username: 'multi-trx1'}) + trx1.rollback() + + const user = yield Database.table('users').where('username', 'multi-trx') + const user1 = yield Database.table('users').where('username', 'multi-trx1') + expect(user).to.be.an('array') + expect(user1).to.be.an('array') + expect(user.length).to.equal(1) + expect(user1.length).to.equal(0) + }) + + it('should be able to call beginTransaction to a different connection', function * () { + yield modelFixtures.up(Database.connection('alternateConnection')) + const trx = yield Database.connection('alternateConnection').beginTransaction() + yield trx.table('users').insert({username: 'conn2-trx'}) + trx.commit() + + const user = yield Database.connection('alternateConnection').table('users').where('username', 'conn2-trx') + expect(user).to.be.an('array') + expect(user.length).to.equal(1) + }) + + it('should be able to commit transactions automatically', function * () { + const response = yield Database.transaction(function * (trx) { + return yield trx.table('users').insert({username: 'auto-trx'}) + }) + expect(response).to.be.an('array') + expect(response.length).to.equal(1) + }) + + it('should rollback transactions automatically on error', function * () { + try { + yield Database.transaction(function * (trx) { + return yield trx.table('users').insert({u: 'auto-trx'}) + }) + expect(true).to.equal(false) + } catch (e) { + expect(e.message).not.equal(undefined) + } + }) + + it('should be able to run transactions on different connection', function * () { + yield Database.connection('alternateConnection').transaction(function * (trx) { + return yield trx.table('users').insert({username: 'different-trx'}) + }) + const user = yield Database.connection('alternateConnection').table('users').where('username', 'different-trx') + expect(user).to.be.an('array') + expect(user[0].username).to.equal('different-trx') + }) + + it('should be able to paginate results', function * () { + const paginatedUsers = yield Database.table('users').paginate(1) + expect(paginatedUsers).to.have.property('total') + expect(paginatedUsers).to.have.property('lastPage') + expect(paginatedUsers).to.have.property('perPage') + expect(paginatedUsers).to.have.property('data') + expect(paginatedUsers.total).to.equal(paginatedUsers.data.length) + }) + + it('should throw an error when page is not passed', function * () { + try { + yield Database.table('users').paginate() + expect(true).to.equal(false) + } catch (e) { + expect(e.message).to.match(/page parameter is required/) + } + }) + + it('should throw an error when page equals 0', function * () { + try { + yield Database.table('users').paginate(0) + expect(true).to.equal(false) + } catch (e) { + expect(e.message).to.match(/cannot paginate results for page less than 1/) + } + }) + + it('should return proper meta data when paginate returns zero results', function * () { + const paginatedUsers = yield Database.table('users').where('status', 'published').paginate(1) + expect(paginatedUsers.total).to.equal(0) + expect(paginatedUsers.lastPage).to.equal(0) + }) + + it('should return proper meta data when there are results but page is over the last page', function * () { + const paginatedUsers = yield Database.table('users').paginate(10) + expect(paginatedUsers.total).to.equal(3) + expect(paginatedUsers.lastPage).to.equal(1) + }) + + it('should be able to get results in chunks', function * () { + let callbackCalledForTimes = 0 + const allUsers = yield Database.table('users') + yield Database.table('users').chunk(1, function (user) { + expect(user[0].id).to.equal(allUsers[callbackCalledForTimes].id) + callbackCalledForTimes++ + }) + expect(callbackCalledForTimes).to.equal(allUsers.length) + }) +}) diff --git a/test/unit/fixtures/files.js b/test/unit/fixtures/files.js new file mode 100644 index 00000000..db2b992c --- /dev/null +++ b/test/unit/fixtures/files.js @@ -0,0 +1,20 @@ +'use strict' + +/** + * adonis-lucid + * Copyright(c) 2015-2015 Harminder Virk + * MIT Licensed +*/ + +const fs = require('co-fs-extra') +const path = require('path') + +module.exports = { + cleanStorage: function * () { + return yield fs.emptyDir(path.join(__dirname,'../storage')) + }, + createDir: function * () { + return yield fs.ensureDir(path.join(__dirname,'../storage')) + } +} + diff --git a/test/unit/fixtures/model.js b/test/unit/fixtures/model.js new file mode 100644 index 00000000..59962af1 --- /dev/null +++ b/test/unit/fixtures/model.js @@ -0,0 +1,85 @@ +'use strict' + +const bluebird = require('bluebird') + +module.exports = { + up: function (knex) { + const tables = [ + knex.schema.createTable('users', function (table) { + table.increments() + table.string('username') + table.string('firstname') + table.string('lastname') + table.string('status') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('accounts', function (table) { + table.increments() + table.string('account_name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('profiles', function (table) { + table.increments() + table.integer('user_id') + table.string('display_name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('cars', function (table) { + table.increments() + table.integer('user_id') + table.string('car_name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('keys', function (table) { + table.increments() + table.integer('car_id') + table.string('key_number') + table.timestamps() + table.timestamp('deleted_at').nullable() + }) + ] + return bluebird.all(tables) + }, + + down: function (knex) { + const dropTables = [ + knex.schema.dropTable('users'), + knex.schema.dropTable('accounts'), + knex.schema.dropTable('profiles'), + knex.schema.dropTable('cars'), + knex.schema.dropTable('keys') + ] + return bluebird.all(dropTables) + }, + + setupAccount: function (knex) { + return knex.table('accounts').insert({account_name: 'sales', created_at: new Date(), updated_at: new Date()}) + }, + + setupProfile: function (knex) { + return knex.table('profiles').insert({user_id: 1, display_name: 'virk', created_at: new Date(), updated_at: new Date()}) + }, + + setupCar: function (knex) { + return knex.table('cars').insert({user_id: 1, car_name: 'audi a6', created_at: new Date(), updated_at: new Date()}) + }, + + setupCarKey: function (knex) { + return knex.table('keys').insert({car_id: 1, key_number: '98010291222', created_at: new Date(), updated_at: new Date()}) + }, + + setupUser: function (knex) { + return knex.table('users').insert({ + firstname: 'aman', + lastname: 'virk', + username: 'avirk', + created_at: new Date(), + updated_at: new Date() + }) + } + +} diff --git a/test/unit/fixtures/relations.js b/test/unit/fixtures/relations.js new file mode 100644 index 00000000..28b598b7 --- /dev/null +++ b/test/unit/fixtures/relations.js @@ -0,0 +1,77 @@ +'use strict' + +/** + * adonis-lucid + * Copyright(c) 2016-2016 Harminder Virk + * MIT Licensed +*/ + +const path = require('path') +const bluebird = require('bluebird') +const files = require('./files') + +module.exports = { + setupTables: function (knex) { + const tables = [ + knex.schema.createTable('suppliers', function (table) { + table.increments() + table.string('name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('accounts', function (table) { + table.increments() + table.integer('supplier_id') + table.string('name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('all_suppliers', function (table) { + table.increments() + table.string('regid').unique() + table.string('name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('all_accounts', function (table) { + table.increments() + table.string('supplier_regid') + table.string('name') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), + knex.schema.createTable('users', function (table) { + table.increments() + table.string('username') + table.integer('manager_id') + table.string('type') + table.timestamps() + table.timestamp('deleted_at').nullable() + }) + ] + return bluebird.all(tables) + }, + dropTables: function (knex) { + const tables = [ + knex.schema.dropTable('accounts'), + knex.schema.dropTable('suppliers'), + knex.schema.dropTable('all_accounts'), + knex.schema.dropTable('all_suppliers'), + knex.schema.dropTable('users') + ] + return bluebird.all(tables) + }, + createRecords: function * (knex, table, values) { + return yield knex.table(table).insert(values) + }, + truncate: function * (knex, table) { + yield knex.table(table).truncate() + }, + up: function * (knex) { + yield files.createDir() + yield this.setupTables(knex) + }, + down: function * (knex) { + yield this.dropTables(knex) + } +} diff --git a/test/unit/helpers/config.js b/test/unit/helpers/config.js new file mode 100644 index 00000000..4c1ae09d --- /dev/null +++ b/test/unit/helpers/config.js @@ -0,0 +1,34 @@ +'use strict' + +/** + * adonis-lucid + * Copyright(c) 2016-2016 Harminder Virk + * MIT Licensed +*/ +const path = require('path') +const mysqlConnections = require('./mysqlConnections') +const sqliteConnections = require('./sqliteConnections') + +module.exports = { + get: function (key) { + if (key === 'database.connection') { + return process.env.DB + } + + if (key === 'database.sqlite3') { + return sqliteConnections.default + } + + if (key === 'database.mysql') { + return mysqlConnections.default + } + + if (key === 'database.alternateConnection' && process.env.DB === 'sqlite3') { + return sqliteConnections.alternateConnection + } + + if (key === 'database.alternateConnection' && process.env.DB === 'mysql') { + return mysqlConnections.alternateConnection + } + } +} diff --git a/test/unit/helpers/mysqlConnections.js b/test/unit/helpers/mysqlConnections.js new file mode 100644 index 00000000..d06943c8 --- /dev/null +++ b/test/unit/helpers/mysqlConnections.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * adonis-lucid + * Copyright(c) 2016-2016 Harminder Virk + * MIT Licensed +*/ + +module.exports = { + default : { + client: 'mysql', + connection: { + user : 'root', + password : '', + database : 'default' + } + }, + + alternateConnection: { + client: 'mysql', + connection: { + user : 'root', + password : '', + database : 'alternate' + } + } +} diff --git a/test/unit/helpers/query.js b/test/unit/helpers/query.js new file mode 100644 index 00000000..d14d01ae --- /dev/null +++ b/test/unit/helpers/query.js @@ -0,0 +1,16 @@ +'use strict' + +/** + * adonis-lucid + * Copyright(c) 2016-2016 Harminder Virk + * MIT Licensed +*/ + +module.exports = { + formatQuery: function (query) { + if(process.env.DB === 'mysql') { + return query.replace(/"/g, '`') + } + return query + } +} diff --git a/test/unit/helpers/sqliteConnections.js b/test/unit/helpers/sqliteConnections.js new file mode 100644 index 00000000..a4ff49ba --- /dev/null +++ b/test/unit/helpers/sqliteConnections.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * adonis-lucid + * Copyright(c) 2016-2016 Harminder Virk + * MIT Licensed +*/ +const path = require('path') + +module.exports = { + default : { + client: 'sqlite3', + connection: { + filename: path.join(__dirname, '../storage/test.sqlite3') + }, + useNullAsDefault: true, + debug: false + }, + + alternateConnection: { + client: 'sqlite3', + connection: { + filename: path.join(__dirname, '../storage/test2.sqlite3') + }, + useNullAsDefault: true + } +} diff --git a/test/unit/storage/test.sqlite3 b/test/unit/storage/test.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..6a892b10f395f1c091788128ed9bde91dac11236 GIT binary patch literal 9216 zcmeHN&u`N(6t?3wU63XOXo@&gSUI)(aRKelK)nrVgIzcUH}N1MNjt|u^w{-Z;k>_t zD_0O#q+Q^|2?_D+EK(CXX{yM9)~;TheCf0Dea=&y_wwTSFp`{}jzq#FeS}&FWAvC( zge(ssofUkN265o*IdwaBx#z*F&&F2 zVPZnx@(E?R97Sm$c*0Y;OT9ef!k_a3D2%dk%qD)y67JD=OayP3=sn!u@54#(WRizI zlO7FWPeut>!;ICzUvS7{UWfHH^{3BTn&Nq)DY76K<*CfdzE!1I!b?%Sa&DH-`7P3M zT^G+X%}2?a>5WxoqOo}z)8kIZ-P*#lwswR$%9L$X-4rzMb>sOBSbch7C_l}UAs3BJ z?vZ7?J3IKLW|W=95W0TG&vKpyymm+`JZmR=>K5Du&%TOHLOvq$gM5L`G^2n~;6GI0 z4slSoxNh|%+LPk_UCTia3Mk2A87b^GcF_F-3x^6&@volh=zu;L0hn7CLk zm`EA_E6&)^GYS|57ARoie}O0lG77AT0w(^~WTFjCqrd_MHt{D!&^F}e(QoL~uWZvJ zyGWoCoo-ykd}@s%JtcwDwJZ^!h`hFV>>itMz8(^mWa|O&=YVOO3rN5_Wd@Z#G GD~dlv9#~}n literal 0 HcmV?d00001 diff --git a/test/unit/storage/test2.sqlite3 b/test/unit/storage/test2.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..9cfe0d24d3b53037f7d767b202a533152c297ccf GIT binary patch literal 9216 zcmeHN&uiN-6qc;SX$HM)bUu{{UzYU8rK9(CBexFPt(ToV)mAn$TTW$pEXOqeO3(dY z3OfyU9TdiHgTY|z$*CdsQYc|QESAEv>`!?j-zPt;yysWPhoRzhn8=tZ`UJHQ#^@=f z2;rXyq3(@!;Q9j2TVO@Q#mVhG%&~iYy$)Z%ofUFNho~DV=mMVb+eR9Z_W##AWTOQ8+(Gq+@&9w^xrYr+uPso!ytGv#RHEi zmj-aA!kFu6M*89}IN%Yl!ufCNPhPZgisx}ok@&OskXJUn}+7S?!3M}t1d50<%ujl<+8TL zJ+f?PdmF#b8Kq|tgszwJvy2O$S1w73XJxRbZ^0e#?CaRH$R|X;!ND}6fKlMTSKy)5 zMUQ7Jf^azG5}ruAN`AbLyXesj_Y)!ZG^XQUKh?>P*?>731&jhKu0R(P2hTQ9Mf|Tg zV?)mK1|)qowvAKWom AfdBvi literal 0 HcmV?d00001 diff --git a/test/unit/util.spec.js b/test/unit/util.spec.js new file mode 100644 index 00000000..d4066af8 --- /dev/null +++ b/test/unit/util.spec.js @@ -0,0 +1,115 @@ +'use strict' + +/** + * adonis-framework + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +/* global describe, it*/ +const util = require('../../lib/util') +const chai = require('chai') +const expect = chai.expect + +describe('Utils', function () { + it('should make plural table for a given model', function () { + class Person {} + const tableName = util.makeTableName(Person) + expect(tableName).to.equal('people') + }) + + it('should convert CamelCase models table to underscore table names', function () { + class LineItem {} + const tableName = util.makeTableName(LineItem) + expect(tableName).to.equal('line_items') + }) + + it('should convert proper plural names', function () { + class Mouse {} + const tableName = util.makeTableName(Mouse) + expect(tableName).to.equal('mice') + }) + + it('should make model foriegn key', function () { + class Mouse {} + const foreignKey = util.makeForeignKey(Mouse) + expect(foreignKey).to.equal('mouse_id') + }) + + it('should make model foriegn key to underscore when model name is CamelCase', function () { + class LineItems {} + const foreignKey = util.makeForeignKey(LineItems) + expect(foreignKey).to.equal('line_item_id') + }) + + it('should make getter name for a given field', function () { + const field = 'id' + const idGetter = util.makeGetterName(field) + expect(idGetter).to.equal('getId') + }) + + it('should make getter name for a given field with snake case name', function () { + const field = 'user_name' + const idGetter = util.makeGetterName(field) + expect(idGetter).to.equal('getUserName') + }) + + it('should make getter name for a given field with dash case name', function () { + const field = 'first-name' + const idGetter = util.makeGetterName(field) + expect(idGetter).to.equal('getFirstName') + }) + + it('should return offset to be used inside a query for a given page', function () { + const offset = util.returnOffset(1, 20) + expect(offset).to.equal(0) + const pageNextOffset = util.returnOffset(2, 20) + expect(pageNextOffset).to.equal(20) + const pageThirdOffset = util.returnOffset(3, 20) + expect(pageThirdOffset).to.equal(40) + const pageLastOffset = util.returnOffset(100, 20) + expect(pageLastOffset).to.equal(1980) + }) + + it('return a new array with values of defined key', function () { + const original = [ + { + username: 'foo', + age: 22 + }, + { + username: 'bar', + age: 24 + } + ] + const transformed = util.mapValuesForAKey(original, 'username') + expect(transformed).deep.equal(['foo', 'bar']) + }) + + it('should return error when page number is less than zero', function () { + const fn = function () { + return util.validatePage(0) + } + expect(fn).to.throw(/cannot paginate results for page less than 1/) + }) + + it('should return error when page number is not a number than zero', function () { + const fn = function () { + return util.validatePage('1') + } + expect(fn).to.throw(/page parameter is required to paginate results/) + }) + + it('should make paginate meta data when total results are zero', function () { + const meta = util.makePaginateMeta(0, 1, 10) + expect(meta).deep.equal({total: 0, perPage: 10, currentPage: 1, lastPage: 0, data: []}) + }) + + it('should make paginate meta data when total results are more than zero', function () { + const meta = util.makePaginateMeta(20, 1, 10) + expect(meta).deep.equal({total: 20, perPage: 10, currentPage: 1, lastPage: 2, data: []}) + }) +})