diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index db0ff575..a0890a78 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -11,6 +11,7 @@ const _ = require('lodash') const moment = require('moment') +const { resolver } = require('@adonisjs/fold') const Hooks = require('../Hooks') const QueryBuilder = require('../QueryBuilder') @@ -278,7 +279,7 @@ class Model { * @static */ static query () { - return new QueryBuilder(this, this.connection) + return new (this.QueryBuilder || QueryBuilder)(this, this.connection) } /** @@ -293,6 +294,7 @@ class Model { */ static boot () { this.hydrate() + _.each(this.traits, (trait) => this.addTrait(trait)) } /** @@ -329,6 +331,39 @@ class Model { * query builder queries. */ this.$globalScopes = [] + + /** + * We use the default query builder class to run queries, but as soon + * as someone wants to add methods to the query builder via traits, + * we need an isolated copy of query builder class just for that + * model, so that the methods added via traits are not impacting + * other models. + */ + this.QueryBuilder = null + } + + /** + * Define a query macro to be added to query builder. + * + * @method queryMacro + * + * @param {String} name + * @param {Function} fn + * + * @chainable + */ + static queryMacro (name, fn) { + /** + * Someone wished to add methods to query builder but just for + * this model. First get a unique copy of query builder and + * then add methods to it's prototype. + */ + if (!this.QueryBuilder) { + this.QueryBuilder = class ExtendedQueryBuilder extends QueryBuilder {} + } + + this.QueryBuilder.prototype[name] = fn + return this } /** @@ -388,6 +423,24 @@ class Model { return this } + /** + * Adds a new trait to the model. Ideally it does a very + * simple thing and that is to pass the model class to + * your trait and your own it from there + * + * @method addTrait + * + * @param {Function|String} trait - A plain function or reference to IoC container string + */ + static addTrait (trait) { + if (typeof (trait) !== 'function' && typeof (trait) !== 'string') { + throw new Error('fuck off') + } + + const { method } = resolver.forDir('modelTraits').resolveFunc(trait) + method(this) + } + /** * This method is executed for all the date fields * with the field name and the value. The return diff --git a/test/unit/traits.spec.js b/test/unit/traits.spec.js new file mode 100644 index 00000000..8d5958c9 --- /dev/null +++ b/test/unit/traits.spec.js @@ -0,0 +1,221 @@ +'use strict' + +/* + * adonis-lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +const test = require('japa') +const { ioc } = require('@adonisjs/fold') +const { setupResolver, Config } = require('@adonisjs/sink') +const helpers = require('./helpers') +const Model = require('../../src/Lucid/Model') +const DatabaseManager = require('../../src/Database/Manager') +const QueryBuilder = require('../../src/Lucid/QueryBuilder') + +test.group('Traits', (group) => { + group.before(() => { + ioc.singleton('Adonis/Src/Database', function () { + const config = new Config() + config.set('database', { + connection: 'testing', + testing: helpers.getConfig() + }) + return new DatabaseManager(config) + }) + ioc.alias('Adonis/Src/Database', 'Database') + + setupResolver() + }) + + group.beforeEach(() => { + ioc.restore() + }) + + test('add a trait to the model', (assert) => { + assert.plan(1) + + class User extends Model { + static boot () { + super.boot() + this.addTrait((ctx) => { + assert.deepEqual(ctx, User) + }) + } + } + + User._bootIfNotBooted() + }) + + test('trait should be called only once', (assert) => { + assert.plan(1) + + class User extends Model { + static boot () { + super.boot() + this.addTrait((ctx) => { + assert.deepEqual(ctx, User) + }) + } + } + + User._bootIfNotBooted() + User._bootIfNotBooted() + }) + + test('bind trait via ioc container', (assert) => { + assert.plan(1) + + class FooTrait { + register (ctx) { + assert.deepEqual(ctx, User) + } + } + + ioc.fake('FooTrait', () => { + return new FooTrait() + }) + + class User extends Model { + static boot () { + super.boot() + this.addTrait('@provider:FooTrait.register') + } + } + + User._bootIfNotBooted() + }) + + test('bind from a specific dir', (assert) => { + assert.plan(1) + + class FooTrait { + register (ctx) { + assert.deepEqual(ctx, User) + } + } + + ioc.fake('App/Models/Traits/FooTrait', () => { + return new FooTrait() + }) + + class User extends Model { + static boot () { + super.boot() + this.addTrait('FooTrait.register') + } + } + + User._bootIfNotBooted() + }) + + test('define traits as an array on model', (assert) => { + assert.plan(1) + + class FooTrait { + register (ctx) { + assert.deepEqual(ctx, User) + } + } + + ioc.fake('App/Models/Traits/FooTrait', () => { + return new FooTrait() + }) + + class User extends Model { + static get traits () { + return ['FooTrait.register'] + } + } + + User._bootIfNotBooted() + }) + + test('define methods on query builder', (assert) => { + class User extends Model { + static boot () { + super.boot() + this.addTrait((ctx) => { + ctx.queryMacro('search', function (name, value) { + return this.where(name, value) + }) + }) + } + } + + User._bootIfNotBooted() + + const userQuery = User.query().search('username', 'virk').toSQL() + assert.equal(userQuery.sql, helpers.formatQuery('select * from "users" where "username" = ?')) + assert.deepEqual(userQuery.bindings, helpers.formatBindings(['virk'])) + }) + + test('model should not have an isolated copy of query builder unless queryMacro is defined', (assert) => { + class User extends Model { + static boot () { + super.boot() + this.addTrait(function () {}) + } + } + + User._bootIfNotBooted() + assert.isNull(User.QueryBuilder) + }) + + test('adding queryMacro to one model should not effect other models', (assert) => { + class User extends Model { + static boot () { + super.boot() + this.addTrait(function (ctx) { + ctx.queryMacro('foo', function () {}) + }) + } + } + + class Profile extends Model {} + + User._bootIfNotBooted() + Profile._bootIfNotBooted() + + assert.isFunction(User.QueryBuilder.prototype.foo) + assert.isUndefined(QueryBuilder.prototype.foo) + }) + + test('should be able to define isolated query macros for all the models', (assert) => { + const stack = [] + + class User extends Model { + static boot () { + super.boot() + this.addTrait(function (ctx) { + ctx.queryMacro('foo', function () { + stack.push('on user') + }) + }) + } + } + + class Profile extends Model { + static boot () { + super.boot() + this.addTrait(function (ctx) { + ctx.queryMacro('foo', function () { + stack.push('on profile') + }) + }) + } + } + + User._bootIfNotBooted() + Profile._bootIfNotBooted() + + User.query().foo() + Profile.query().foo() + + assert.deepEqual(stack, ['on user', 'on profile']) + }) +})