Skip to content

Commit

Permalink
feat(traits): add support for traits
Browse files Browse the repository at this point in the history
traits can extended individual models and query builder for that model. Awesome stuff
  • Loading branch information
thetutlage committed Jun 30, 2017
1 parent a6b6788 commit bced30d
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 1 deletion.
55 changes: 54 additions & 1 deletion src/Lucid/Model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

const _ = require('lodash')
const moment = require('moment')
const { resolver } = require('@adonisjs/fold')

const Hooks = require('../Hooks')
const QueryBuilder = require('../QueryBuilder')
Expand Down Expand Up @@ -278,7 +279,7 @@ class Model {
* @static
*/
static query () {
return new QueryBuilder(this, this.connection)
return new (this.QueryBuilder || QueryBuilder)(this, this.connection)
}

/**
Expand All @@ -293,6 +294,7 @@ class Model {
*/
static boot () {
this.hydrate()
_.each(this.traits, (trait) => this.addTrait(trait))
}

/**
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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
Expand Down
221 changes: 221 additions & 0 deletions test/unit/traits.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
'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 { 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'])
})
})

0 comments on commit bced30d

Please sign in to comment.