Skip to content

Commit

Permalink
feat(query): add support for withCount
Browse files Browse the repository at this point in the history
withCount will sideload the count of relations
  • Loading branch information
thetutlage committed Jun 23, 2017
1 parent 0ff24e7 commit b87eb40
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/Exceptions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ class RuntimeException extends NE.RuntimeException {
static undefinedRelation (relation, name) {
return new this(`${relation} is not defined on ${name} model`, 500, 'E_INVALID_MODEL_RELATION')
}

static cannotNestRelation (relation, parent, method) {
const message = `${method} does not allowed nested relations. Instead use .with('${parent}', (builder) => builder.${method}('${relation}'))`
return new this(message, 500, 'E_CANNOT_NEST_RELATION')
}
}

class InvalidArgumentException extends NE.InvalidArgumentException {
Expand Down
3 changes: 2 additions & 1 deletion src/Lucid/Model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,9 +488,10 @@ class Model {
* @private
*/
_instantiate () {
this.__setters__ = ['$attributes', '$persisted', 'primaryKeyValue', '$originalAttributes', '$relations']
this.__setters__ = ['$attributes', '$persisted', 'primaryKeyValue', '$originalAttributes', '$relations', '$sideLoaded']
this.$relations = {}
this.$attributes = {}
this.$sideLoaded = {}
this.$originalAttributes = {}
this.$persisted = false
}
Expand Down
5 changes: 5 additions & 0 deletions src/Lucid/Model/proxyHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ proxyHandler.get = function (target, name) {
if (typeof (target[name]) !== 'undefined') {
return target[name]
}

if (typeof (target.$sideLoaded[name]) !== 'undefined') {
return target.$sideLoaded[name]
}

return target.$attributes[name]
}
48 changes: 46 additions & 2 deletions src/Lucid/QueryBuilder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const _ = require('lodash')
const util = require('../../../lib/util')
const EagerLoad = require('../EagerLoad')
const RelationsParser = require('../Relations/Parser')
const CE = require('../../Exceptions')

const proxyHandler = {
get (target, name) {
Expand Down Expand Up @@ -45,6 +46,7 @@ class QueryBuilder {
this.query = this.db.table(table).on('query', this.model._executeListeners.bind(this.model))
this._ignoreScopes = []
this._eagerLoads = {}
this._sideLoaded = []
return new Proxy(this, proxyHandler)
}

Expand Down Expand Up @@ -94,7 +96,7 @@ class QueryBuilder {
const { name, nested } = RelationsParser.parseRelation(relation)
RelationsParser.validateRelationExistence(this.model.prototype, name)
const relationInstance = RelationsParser.getRelatedInstance(this.model.prototype, name)
return { relationInstance, nested }
return { relationInstance, nested, name }
}

/**
Expand Down Expand Up @@ -135,7 +137,12 @@ class QueryBuilder {
_mapRowsToInstances (rows) {
return rows.map((row) => {
const modelInstance = new this.model()
modelInstance.newUp(row)
modelInstance.newUp(_.omitBy(row, (value, field) => {
if (this._sideLoaded.indexOf(field) > -1) {
modelInstance.$sideLoaded[field] = value
return true
}
}))
return modelInstance
})
}
Expand Down Expand Up @@ -548,6 +555,43 @@ class QueryBuilder {

return this
}

/**
* Returns count of a relationship
*
* @method withCount
*
* @param {String} relation
* @param {Function} callback
*
* @chainable
*/
withCount (relation, callback) {
let { name, nested } = RelationsParser.parseRelation(relation)
if (nested) {
throw CE.RuntimeException.cannotNestRelation(_.first(_.keys(nested)), name, 'withCount')
}

const tokens = name.match(/as\s(\w+)/)
let asStatement = `${name}_count`
if (_.size(tokens)) {
asStatement = tokens[1]
name = name.replace(tokens[0], '').trim()
}

RelationsParser.validateRelationExistence(this.model.prototype, name)
const relationInstance = RelationsParser.getRelatedInstance(this.model.prototype, name)

if (typeof (callback) === 'function') {
callback(relationInstance)
}

const columns = _.find(this.query._statement, (statement) => statement.grouping === 'columns') || ['*']
this._sideLoaded.push(asStatement)
columns.push(relationInstance.relatedWhere(true).as(asStatement))
this.query.select(columns)
return this
}
}

module.exports = QueryBuilder
8 changes: 7 additions & 1 deletion src/Lucid/Serializers/Collection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const _ = require('lodash')

class Collection {
constructor (rows, pages = null, isOne = false) {
this.rows = rows
Expand All @@ -8,7 +10,11 @@ class Collection {
}

first () {
return this.rows[0]
return _.first(this.rows)
}

last () {
return _.last(this.rows)
}

size () {
Expand Down
167 changes: 167 additions & 0 deletions test/unit/lucid-relations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1035,4 +1035,171 @@ test.group('Relations | HasOne', (group) => {
assert.deepEqual(users.pages, { lastPage: 1, perPage: 20, total: 1, page: 1 })
assert.equal(userQuery.sql, helpers.formatQuery('select * from "users" where (select count(*) from "profiles" where users.id = profiles.user_id) = ? limit ?'))
})

test('return relation count', async (assert) => {
class Profile extends Model {
}

class User extends Model {
profile () {
return this.hasOne(Profile)
}
}

User._bootIfNotBooted()
Profile._bootIfNotBooted()

let userQuery = null
User.onQuery((query) => userQuery = query)

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await ioc.use('Database').table('profiles').insert([{ user_id: 1, likes: 3 }])

const users = await User.query().withCount('profile').fetch()
assert.equal(users.size(), 2)
assert.equal(users.first().profile_count, 1)
assert.deepEqual(users.first().$sideLoaded, { profile_count: 1 })
assert.equal(userQuery.sql, helpers.formatQuery('select *, (select count(*) from "profiles" where users.id = profiles.user_id) as "profile_count" from "users"'))
})

test('return relation count with paginate method', async (assert) => {
class Profile extends Model {
}

class User extends Model {
profile () {
return this.hasOne(Profile)
}
}

User._bootIfNotBooted()
Profile._bootIfNotBooted()

let userQuery = null
User.onQuery((query) => userQuery = query)

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await ioc.use('Database').table('profiles').insert([{ user_id: 1, likes: 3 }])

const users = await User.query().withCount('profile').paginate()
assert.equal(users.size(), 2)
assert.equal(users.first().profile_count, 1)
assert.deepEqual(users.first().$sideLoaded, { profile_count: 1 })
assert.equal(userQuery.sql, helpers.formatQuery('select *, (select count(*) from "profiles" where users.id = profiles.user_id) as "profile_count" from "users" limit ?'))
})

test('define count column for withCount', async (assert) => {
class Profile extends Model {
}

class User extends Model {
profile () {
return this.hasOne(Profile)
}
}

User._bootIfNotBooted()
Profile._bootIfNotBooted()

let userQuery = null
User.onQuery((query) => userQuery = query)

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await ioc.use('Database').table('profiles').insert([{ user_id: 1, likes: 3 }])

const users = await User.query().withCount('profile as my_profile').fetch()
assert.equal(users.size(), 2)
assert.equal(users.first().my_profile, 1)
assert.deepEqual(users.first().$sideLoaded, { my_profile: 1 })
assert.equal(userQuery.sql, helpers.formatQuery('select *, (select count(*) from "profiles" where users.id = profiles.user_id) as "my_profile" from "users"'))
})

test('define callback with withCount', async (assert) => {
class Profile extends Model {
}

class User extends Model {
profile () {
return this.hasOne(Profile)
}
}

User._bootIfNotBooted()
Profile._bootIfNotBooted()

let userQuery = null
User.onQuery((query) => userQuery = query)

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await ioc.use('Database').table('profiles').insert([{ user_id: 1, likes: 3 }])

const users = await User.query().withCount('profile', function (builder) {
builder.where('likes', '>', 3)
}).fetch()
assert.equal(users.size(), 2)
assert.equal(users.first().profile_count, 0)
assert.deepEqual(users.first().$sideLoaded, { profile_count: 0 })
assert.equal(userQuery.sql, helpers.formatQuery('select *, (select count(*) from "profiles" where "likes" > ? and users.id = profiles.user_id) as "profile_count" from "users"'))
})

test('throw exception when trying to call withCount with nested relations', async (assert) => {
class Profile extends Model {
}

class User extends Model {
profile () {
return this.hasOne(Profile)
}
}

User._bootIfNotBooted()
Profile._bootIfNotBooted()

let userQuery = null
User.onQuery((query) => userQuery = query)

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await ioc.use('Database').table('profiles').insert([{ user_id: 1, likes: 3 }])

const users = () => User.query().withCount('profile.picture')
assert.throw(users, `E_CANNOT_NEST_RELATION: withCount does not allowed nested relations. Instead use .with('profile', (builder) => builder.withCount('picture'))`)
})

test('allow withCount on nested query builder', async (assert) => {
class Picture extends Model {

}

class Profile extends Model {
picture () {
return this.hasOne(Picture)
}
}

class User extends Model {
profile () {
return this.hasOne(Profile)
}
}

User._bootIfNotBooted()
Profile._bootIfNotBooted()
Picture._bootIfNotBooted()

let userQuery = null
let profileQuery = null
User.onQuery((query) => userQuery = query)
Profile.onQuery((query) => profileQuery = query)

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await ioc.use('Database').table('profiles').insert([{ user_id: 1, likes: 3 }])
await ioc.use('Database').table('pictures').insert([{ profile_id: 1, storage_path: '/foo' }])

const users = await User.query().with('profile', (builder) => builder.withCount('picture')).fetch()
assert.equal(users.size(), 2)
assert.equal(users.first().getRelated('profile').picture_count, 1)
assert.deepEqual(users.first().getRelated('profile').$sideLoaded, { picture_count: 1 })
assert.equal(userQuery.sql, helpers.formatQuery('select * from "users"'))
assert.equal(profileQuery.sql, helpers.formatQuery('select *, (select count(*) from "pictures" where profiles.id = pictures.profile_id) as "picture_count" from "profiles" where "user_id" in (?, ?)'))
})
})

0 comments on commit b87eb40

Please sign in to comment.