Skip to content

Commit

Permalink
fix(queryscopes): ensure query scopes are called with relations too
Browse files Browse the repository at this point in the history
fix #261
  • Loading branch information
thetutlage committed Dec 23, 2017
1 parent 97bd2c3 commit 4d25fcc
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/Lucid/QueryBuilder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ class QueryBuilder {
*/
this._ignoreScopes = []

/**
* A flag to find if scopes has already
* been applied or not
*
* @type {Boolean}
*/
this._scopesApplied = false

/**
* Relations to be eagerloaded
*
Expand Down Expand Up @@ -171,6 +179,8 @@ class QueryBuilder {
} else {
this.query[method](relationInstance.relatedWhere())
}

relationInstance.applyRelatedScopes()
}

/**
Expand Down Expand Up @@ -202,6 +212,18 @@ class QueryBuilder {
* @private
*/
_applyScopes () {
/**
* Do not apply if already applied
*/
if (this._scopesApplied) {
return this
}

/**
* Setting flag to true
*/
this._scopesApplied = true

if (this._ignoreScopes.indexOf('*') > -1) {
return this
}
Expand Down Expand Up @@ -839,6 +861,7 @@ class QueryBuilder {
if (!_.find(this.query._statements, (statement) => statement.grouping === 'columns')) {
columns.push('*')
}

columns.push(relationInstance.relatedWhere(true, this._withCountCounter).as(asStatement))

/**
Expand All @@ -853,6 +876,8 @@ class QueryBuilder {
*/
this.query.select(columns)

relationInstance.applyRelatedScopes()

return this
}

Expand Down
22 changes: 22 additions & 0 deletions src/Lucid/Relations/BaseRelation.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ class BaseRelation {
this.foreignKey = foreignKey
this.relatedQuery = this.RelatedModel.query()

/**
* Storing relation meta-data on the
* query builder.
*/
this.relatedQuery.$relation = {
name: this.constructor.name,
foreignKey: this.foreignKey,
primaryKey: this.primaryKey,
primaryTable: this.$primaryTable,
foreignTable: this.$foreignTable
}

/**
* this is default value to eagerload data, but users
* can pass their custom function by calling
Expand Down Expand Up @@ -174,6 +186,16 @@ class BaseRelation {
}
}

/**
* Applies scopes on the related query. This is used when
* the related query is used as subquery.
*
* @method applyRelatedScopes
*/
applyRelatedScopes () {
this.relatedQuery._applyScopes()
}

/**
* Returns the eagerLoad query for the relationship
*
Expand Down
4 changes: 4 additions & 0 deletions src/Lucid/Relations/BelongsToMany.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class BelongsToMany extends BaseRelation {
this._makeJoinQuery()
this.whereInPivot(fk, values)
}

this.relatedQuery.$relation.pivot = this._pivot
this.relatedQuery.$relation.relatedForeignKey = relatedForeignKey
this.relatedQuery.$relation.relatedPrimaryKey = relatedPrimaryKey
}

/**
Expand Down
76 changes: 76 additions & 0 deletions test/unit/lucid-belongs-to-many.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2089,4 +2089,80 @@ test.group('Relations | Belongs To Many', (group) => {
assert.equal(pivotValues[0].user_party_id, 20)
assert.equal(pivotValues[0].team_party_id, 10)
})

test('apply global scope on related model when eagerloading', async (assert) => {
class Post extends Model {
}

class User extends Model {
posts () {
return this.belongsToMany(Post)
}
}

User._bootIfNotBooted()
Post._bootIfNotBooted()

Post.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

let postQuery = null
Post.onQuery((query) => (postQuery = query))

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await User.query().with('posts').fetch()

assert.equal(postQuery.sql, helpers.formatQuery('select "posts".*, "post_user"."post_id" as "pivot_post_id", "post_user"."user_id" as "pivot_user_id" from "posts" inner join "post_user" on "posts"."id" = "post_user"."post_id" where "post_user"."user_id" in (?, ?) and "posts"."deleted_at" is null'))
})

test('apply global scope on related model when called withCount', async (assert) => {
class Post extends Model {
}

class User extends Model {
posts () {
return this.belongsToMany(Post)
}
}

User._bootIfNotBooted()
Post._bootIfNotBooted()

Post.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

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

await User.query().withCount('posts').fetch()

assert.equal(userQuery.sql, helpers.formatQuery('select *, (select count(*) from "posts" inner join "post_user" on "posts"."id" = "post_user"."post_id" where users.id = post_user.user_id and "posts"."deleted_at" is null) as "posts_count" from "users"'))
})

test('apply global scope on related model when called has', async (assert) => {
class Post extends Model {
}

class User extends Model {
posts () {
return this.belongsToMany(Post)
}
}

User._bootIfNotBooted()
Post._bootIfNotBooted()

Post.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

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

await User.query().has('posts').fetch()

assert.equal(userQuery.sql, helpers.formatQuery('select * from "users" where exists (select * from "posts" inner join "post_user" on "posts"."id" = "post_user"."post_id" where users.id = post_user.user_id and "posts"."deleted_at" is null)'))
})
})
76 changes: 76 additions & 0 deletions test/unit/lucid-belongs-to.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -848,4 +848,80 @@ test.group('Relations | Belongs To', (group) => {

assert.equal(userQuery.sql, helpers.formatQuery(expectedQuery))
})

test('apply global scope on related model when eagerloading', async (assert) => {
class User extends Model {
}

class Car extends Model {
user () {
return this.belongsTo(User)
}
}

User._bootIfNotBooted()
Car._bootIfNotBooted()

User.addGlobalScope(function (builder) {
builder.where('deleted_at', null)
})

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

await ioc.use('Database').table('cars').insert({ name: 'E180', model: 'Mercedes', user_id: 1 })

await Car.query().with('user').fetch()
assert.equal(userQuery.sql, helpers.formatQuery('select * from "users" where "id" in (?) and "deleted_at" is null'))
})

test('apply global scope on related model when called withCount', async (assert) => {
class User extends Model {
}

class Car extends Model {
user () {
return this.belongsTo(User)
}
}

User._bootIfNotBooted()
Car._bootIfNotBooted()

User.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

let carQuery = null
Car.onQuery((query) => (carQuery = query))

await Car.query().withCount('user').fetch()

assert.equal(carQuery.sql, helpers.formatQuery('select *, (select count(*) from "users" where cars.user_id = users.id and "users"."deleted_at" is null) as "user_count" from "cars"'))
})

test('apply global scope on related model when called has', async (assert) => {
class User extends Model {
}

class Car extends Model {
user () {
return this.belongsTo(User)
}
}

User._bootIfNotBooted()
Car._bootIfNotBooted()

User.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

let carQuery = null
Car.onQuery((query) => (carQuery = query))

await Car.query().has('user').fetch()

assert.equal(carQuery.sql, helpers.formatQuery('select * from "cars" where exists (select * from "users" where cars.user_id = users.id and "users"."deleted_at" is null)'))
})
})
77 changes: 77 additions & 0 deletions test/unit/lucid-has-many.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

'use strict'

/*
Expand Down Expand Up @@ -1010,4 +1011,80 @@ test.group('Relations | Has Many', (group) => {
assert.equal(results.last().$sideLoaded.teamMembers_count, 0)
assert.equal(userQuery.sql, helpers.formatQuery(expectedQuery))
})

test('apply global scope on related model when eagerloading', async (assert) => {
class Car extends Model {
}

class User extends Model {
cars () {
return this.hasMany(Car)
}
}

User._bootIfNotBooted()
Car._bootIfNotBooted()

Car.addGlobalScope(function (builder) {
builder.where('deleted_at', null)
})

let carQuery = null
Car.onQuery((query) => (carQuery = query))

await ioc.use('Database').table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await User.query().with('cars').fetch()

assert.equal(carQuery.sql, helpers.formatQuery('select * from "cars" where "user_id" in (?, ?) and "deleted_at" is null'))
})

test('apply global scope on related model when called withCount', async (assert) => {
class Car extends Model {
}

class User extends Model {
cars () {
return this.hasMany(Car)
}
}

User._bootIfNotBooted()
Car._bootIfNotBooted()

Car.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

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

await User.query().withCount('cars').fetch()

assert.equal(userQuery.sql, helpers.formatQuery('select *, (select count(*) from "cars" where users.id = cars.user_id and "cars"."deleted_at" is null) as "cars_count" from "users"'))
})

test('apply global scope on related model when called has', async (assert) => {
class Car extends Model {
}

class User extends Model {
cars () {
return this.hasMany(Car)
}
}

User._bootIfNotBooted()
Car._bootIfNotBooted()

Car.addGlobalScope(function (builder) {
builder.where(`${builder.Model.table}.deleted_at`, null)
})

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

await User.query().has('cars').fetch()

assert.equal(userQuery.sql, helpers.formatQuery('select * from "users" where exists (select * from "cars" where users.id = cars.user_id and "cars"."deleted_at" is null)'))
})
})
Loading

0 comments on commit 4d25fcc

Please sign in to comment.