Skip to content

Commit

Permalink
feat(relations): filter results based upon relations
Browse files Browse the repository at this point in the history
add support for filtering top level results when something is truthy with their related models. Add

methods {has, whereHas, doesntHave, whereDoesHave, withCount}

Closes #92
  • Loading branch information
thetutlage committed Jul 16, 2017
1 parent f9f9410 commit 6146c2a
Show file tree
Hide file tree
Showing 7 changed files with 1,890 additions and 131 deletions.
14 changes: 14 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,17 @@ util.makePivotTableName = function (modelName, relatedModelName) {
relatedModelName = _.snakeCase(pluralize.singular(relatedModelName))
return _.sortBy([modelName, relatedModelName]).join('_')
}

/**
* Parses the nested relations with dot syntax
* and returns an object with root and nested
* relations.
*
* @param {String} relation
*
* @return {Object}
*/
util.parseNestedRelations = function (relations) {
const relationsKeys = relations.split('.')
return { root: relationsKeys[0], nested: (relationsKeys && relationsKeys.length) ? _.tail(relationsKeys).join('.') : null }
}
2 changes: 2 additions & 0 deletions src/Lucid/QueryBuilder/Serializers/Base.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ class BaseSerializer {
currentPage: values.currentPage,
lastPage: values.lastPage
}

const collectionToJSON = collection.toJSON

collection.toJSON = function () {
const meta = collection.meta
meta.data = collectionToJSON.bind(collection)()
Expand Down
163 changes: 163 additions & 0 deletions src/Lucid/QueryBuilder/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

const _ = require('lodash')
const util = require('../../../lib/util')
const CE = require('../../Exceptions')
const methods = exports = module.exports = {}

Expand Down Expand Up @@ -261,6 +262,168 @@ methods.with = function (target) {
}
}

/**
* Filters the top level results by checking the existence
* of related rows.
*
* @param {Object} target
* @return {Object} reference to this for chaining
*
* @private
*/
methods._has = function (target) {
return function (key, method, expression, value) {
if (!value && expression) {
value = expression
expression = '='
}

const relations = util.parseNestedRelations(key)
const relationInstance = target.HostModel.prototype[relations.root]()

/**
* Call the has method on nested relations if any
*/
if (relations.nested) {
relationInstance.getRelatedQuery().has(relations.nested, expression, value)
this[method](relationInstance.exists())
} else if (value && expression) {
this.whereRaw(`(${relationInstance.counts().toSQL().sql}) ${expression} ?`, [value])
} else {
this[method](relationInstance.exists())
}

return this
}
}

/**
* Filters the top level results by checking the existence
* of related rows with additional checks via callback.
*
* @param {Object} target
* @return {Object} reference to this for chaining
*
* @public
*/
methods._whereHas = function (target) {
return function (key, method, callback, expression, value) {
if (!value && expression) {
value = expression
expression = '='
}

const relations = util.parseNestedRelations(key)
const relationInstance = target.HostModel.prototype[relations.root]()

/**
* Call the has method on nested relations if any
*/
if (relations.nested) {
relationInstance.getRelatedQuery().whereHas(relations.nested, callback, expression, value)
this[method](relationInstance.exists())
} else if (value && expression) {
const countsQuery = relationInstance.counts(callback).toSQL()
this.whereRaw(`(${countsQuery.sql}) ${expression} ?`, countsQuery.bindings.concat([value]))
} else {
this[method](relationInstance.exists(callback))
}

return this
}
}

/**
* Filters the top level rows via checking the existence
* of related rows defined as relationships.
*
* @param {String} key
* @param {String} [expression]
* @param {Mixed} [value]
*
* @chainable
*/
methods.has = function () {
return function (key, expression, value) {
return this._has(key, 'whereExists', expression, value)
}
}

/**
* Filters the top level rows via checking the non-existence
* of related rows defined as relationships.
*
* @param {String} key
*
* @chainable
*/
methods.doesntHave = function () {
return function (key) {
return this._has(key, 'whereNotExists')
}
}

/**
* Filters the top level rows via checking the existence
* of related rows defined as relationships and allows
* a conditional callback to add more clauses
*
* @param {String} key
* @param {Function} callback
* @param {String} [expression]
* @param {Mixed} [value]
*
* @chainable
*/
methods.whereHas = function () {
return function (key, callback, value, expression) {
return this._whereHas(key, 'whereExists', callback, value, expression)
}
}

/**
* Filters the top level rows via checking the non-existence
* of related rows defined as relationships and allows
* a conditional callback to add more clauses
*
* @param {String} key
* @param {Function} callback
*
* @chainable
*/
methods.whereDoesntHave = function () {
return function (key, callback) {
return this._whereHas(key, 'whereNotExists', callback)
}
}

methods.withCount = function (target) {
return function (relation, callback) {
const relationInstance = target.HostModel.prototype[relation]()
const selectedColumns = _.find(target.modelQueryBuilder._statements, (statement) => statement.grouping === 'columns')

/**
* Select all columns from the table when none have
* been selected already.
*/
if (!selectedColumns) {
target.modelQueryBuilder.column(`${this.HostModel.table}.*`)
}

/**
* The count query to fetch the related counts
* from relation instance.
*/
const countsQuery = relationInstance.counts(callback).toSQL()
target
.modelQueryBuilder
.column(
target.queryBuilder.raw(countsQuery.sql, countsQuery.bindings).wrap('(', `) as ${relation}_count`)
)
return this
}
}

/**
* stores a callback for a given relation.
*
Expand Down
14 changes: 8 additions & 6 deletions src/Lucid/QueryBuilder/proxyHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ proxyHandler.get = function (target, name) {
* here we try to make a dynamic scope method on query
* builder and if found, we will return that method
*/
const scopeMethod = helpers.getScopeMethod(target.HostModel, name)
if (scopeMethod) {
return function () {
const args = [target.modelQueryBuilder].concat(_.toArray(arguments))
scopeMethod.apply(target.HostModel, args)
return this
if (typeof (name) === 'string') {
const scopeMethod = helpers.getScopeMethod(target.HostModel, name)
if (scopeMethod) {
return function () {
const args = [target.modelQueryBuilder].concat(_.toArray(arguments))
scopeMethod.apply(target.HostModel, args)
return this
}
}
}
return target.modelQueryBuilder[name]
Expand Down
38 changes: 38 additions & 0 deletions src/Lucid/Relations/Relation.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,44 @@ class Relation {
return this.relatedQuery.paginate(page, perPage)
}

/**
* Returns the existence query to be used when main
* query is dependent upon childs.
*
* @param {Function} [callback]
* @return {Object}
*/
exists (callback) {
const relatedQuery = this.relatedQuery.whereRaw(`${this.related.table}.${this.toKey} = ${this.parent.constructor.table}.${this.fromKey}`)
if (typeof (callback) === 'function') {
callback(relatedQuery)
}
return relatedQuery.modelQueryBuilder
}

/**
* Returns the counts query for a given relation
*
* @param {Function} [callback]
* @return {Object}
*/
counts (callback) {
const relatedQuery = this.relatedQuery.count('*').whereRaw(`${this.related.table}.${this.toKey} = ${this.parent.constructor.table}.${this.fromKey}`)
if (typeof (callback) === 'function') {
callback(relatedQuery)
}
return relatedQuery.modelQueryBuilder
}

/**
* Returns the query builder instance for related model.
*
* @return {Object}
*/
getRelatedQuery () {
return this.relatedQuery
}

/**
* adds with clause to related query, it almost becomes a recursive
* loop until we get all nested relations
Expand Down
9 changes: 9 additions & 0 deletions test/unit/fixtures/relations.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
knex.schema.createTable('profiles', function (table) {
table.increments()
table.integer('account_id')
table.boolean('is_primary')
table.string('profile_name')
table.timestamps()
table.timestamp('deleted_at').nullable()
Expand Down Expand Up @@ -77,6 +78,13 @@ module.exports = {
table.timestamps()
table.timestamp('deleted_at').nullable()
}),
knex.schema.createTable('replies', function (table) {
table.increments()
table.integer('comment_id')
table.string('body')
table.timestamps()
table.timestamp('deleted_at').nullable()
}),
knex.schema.createTable('students', function (table) {
table.increments()
table.string('name')
Expand Down Expand Up @@ -141,6 +149,7 @@ module.exports = {
knex.schema.dropTable('users'),
knex.schema.dropTable('posts'),
knex.schema.dropTable('comments'),
knex.schema.dropTable('replies'),
knex.schema.dropTable('courses'),
knex.schema.dropTable('students'),
knex.schema.dropTable('subjects'),
Expand Down

0 comments on commit 6146c2a

Please sign in to comment.