diff --git a/adonis-typings/model.ts b/adonis-typings/model.ts index 64328676..442f57e9 100644 --- a/adonis-typings/model.ts +++ b/adonis-typings/model.ts @@ -323,6 +323,11 @@ declare module '@ioc:Adonis/Lucid/Model' { ExcutableQueryBuilderContract { model: Model + /** + * Define a custom preloader for the current query + */ + usePreloader(preloader: PreloaderContract): this + /** * Whether or not the query is a child query generated for `.where` * callbacks diff --git a/src/Orm/QueryBuilder/index.ts b/src/Orm/QueryBuilder/index.ts index e7232103..988a2136 100644 --- a/src/Orm/QueryBuilder/index.ts +++ b/src/Orm/QueryBuilder/index.ts @@ -17,10 +17,15 @@ import { ModelObject, ModelAdapterOptions, ModelQueryBuilderContract, + LucidRow, } from '@ioc:Adonis/Lucid/Model' import { DBQueryCallback } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder' -import { RelationQueryBuilderContract, RelationshipsContract } from '@ioc:Adonis/Lucid/Relations' +import { + PreloaderContract, + RelationshipsContract, + RelationQueryBuilderContract, +} from '@ioc:Adonis/Lucid/Relations' import { DialectContract, @@ -66,12 +71,12 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon /** * Sideloaded attributes that will be passed to the model instances */ - private sideloaded: ModelObject = {} + protected sideloaded: ModelObject = {} /** * A copy of defined preloads on the model instance */ - private preloader = new Preloader(this.model) + protected preloader: PreloaderContract = new Preloader(this.model) /** * Required by macroable @@ -95,13 +100,13 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon * Custom data someone want to send to the profiler and the * query event */ - private customReporterData: any + protected customReporterData: any /** * Control whether to debug the query or not. The initial * value is inherited from the query client */ - private debugQueries: boolean = this.client.debug + protected debugQueries: boolean = this.client.debug /** * Self join counter, increments with every "withCount" @@ -367,6 +372,11 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon return this } + public usePreloader(preloader: PreloaderContract) { + this.preloader = preloader + return this + } + /** * Set sideloaded properties to be passed to the model instance */ diff --git a/src/Orm/Relations/HasMany/QueryBuilder.ts b/src/Orm/Relations/HasMany/QueryBuilder.ts index cc68a715..6d79a525 100644 --- a/src/Orm/Relations/HasMany/QueryBuilder.ts +++ b/src/Orm/Relations/HasMany/QueryBuilder.ts @@ -146,10 +146,12 @@ export class HasManyQueryBuilder 'adonis_temp' ) - return this.relation - .relatedModel() - .query() - .from(this) - .where(rowName, '<=', this.groupConstraints.limit!) + const groupQuery = this.relation.relatedModel().query() + groupQuery.usePreloader(this.preloader) + groupQuery.sideload(this.sideloaded) + groupQuery.debug(this.debugQueries) + this.customReporterData && groupQuery.reporterData(this.customReporterData) + + return groupQuery.from(this).where(rowName, '<=', this.groupConstraints.limit!) } } diff --git a/src/Orm/Relations/HasManyThrough/QueryBuilder.ts b/src/Orm/Relations/HasManyThrough/QueryBuilder.ts index 170eb87a..994a6274 100644 --- a/src/Orm/Relations/HasManyThrough/QueryBuilder.ts +++ b/src/Orm/Relations/HasManyThrough/QueryBuilder.ts @@ -255,10 +255,12 @@ export class HasManyThroughQueryBuilder 'adonis_temp' ) - return this.relation - .relatedModel() - .query() - .from(this) - .where(rowName, '<=', this.groupConstraints.limit!) + const groupQuery = this.relation.relatedModel().query() + groupQuery.usePreloader(this.preloader) + groupQuery.sideload(this.sideloaded) + groupQuery.debug(this.debugQueries) + this.customReporterData && groupQuery.reporterData(this.customReporterData) + + return groupQuery.from(this).where(rowName, '<=', this.groupConstraints.limit!) } } diff --git a/src/Orm/Relations/ManyToMany/QueryBuilder.ts b/src/Orm/Relations/ManyToMany/QueryBuilder.ts index cd749563..a03d7cfb 100644 --- a/src/Orm/Relations/ManyToMany/QueryBuilder.ts +++ b/src/Orm/Relations/ManyToMany/QueryBuilder.ts @@ -413,10 +413,12 @@ export class ManyToManyQueryBuilder 'adonis_temp' ) - return this.relation - .relatedModel() - .query() - .from(this) - .where(rowName, '<=', this.groupConstraints.limit!) + const groupQuery = this.relation.relatedModel().query() + groupQuery.usePreloader(this.preloader) + groupQuery.sideload(this.sideloaded) + groupQuery.debug(this.debugQueries) + this.customReporterData && groupQuery.reporterData(this.customReporterData) + + return groupQuery.from(this).where(rowName, '<=', this.groupConstraints.limit!) } } diff --git a/test/orm/model-has-many.spec.ts b/test/orm/model-has-many.spec.ts index ee060ed8..b1a6e954 100644 --- a/test/orm/model-has-many.spec.ts +++ b/test/orm/model-has-many.spec.ts @@ -3361,6 +3361,274 @@ if (process.env.DB !== 'mysql_legacy') { assert.equal(sql, knexSql) assert.deepEqual(bindings, knexBindings) }) + + test('preload with group limit', async (assert) => { + class Comment extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public postId: number + + @column() + public body: string + } + + class Post extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + + @column() + public createdAt: Date + + @hasMany(() => Comment) + public comments: HasMany + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @hasMany(() => Post) + public posts: HasMany + } + + await db + .insertQuery() + .table('users') + .insert([{ username: 'virk' }, { username: 'nikk' }]) + + const [user0, user1] = await db.query().from('users') + + /** + * User 1 + */ + await db + .insertQuery() + .table('posts') + .multiInsert([ + { + user_id: user0.id, + title: 'Adonis 101', + created_at: new Date(), + }, + { + user_id: user0.id, + title: 'Adonis 102', + }, + { + user_id: user0.id, + title: 'Adonis 103', + }, + { + user_id: user0.id, + title: 'Adonis 104', + }, + { + user_id: user0.id, + title: 'Adonis 105', + created_at: new Date(), + }, + ]) + + /** + * User 2 + */ + await db + .insertQuery() + .table('posts') + .multiInsert([ + { + user_id: user1.id, + title: 'Lucid 101', + created_at: new Date(), + }, + { + user_id: user1.id, + title: 'Lucid 102', + }, + { + user_id: user1.id, + title: 'Lucid 103', + created_at: new Date(), + }, + { + user_id: user1.id, + title: 'Lucid 104', + created_at: new Date(), + }, + { + user_id: user1.id, + title: 'Lucid 105', + }, + ]) + + User.boot() + const posts = await Post.all() + + await Promise.all( + posts.map((post) => { + return post + .related('comments') + .createMany([{ body: 'Nice post' }, { body: 'Great post' }]) + }) + ) + + const users = await User.query().preload('posts', (query) => { + query.whereNotNull('created_at').groupLimit(2).preload('comments') + }) + + assert.lengthOf(users, 2) + assert.lengthOf(users[0].posts, 2) + assert.lengthOf(users[0].posts[0].comments, 2) + assert.lengthOf(users[0].posts[1].comments, 2) + + assert.lengthOf(users[1].posts, 2) + assert.lengthOf(users[1].posts[0].comments, 2) + assert.lengthOf(users[1].posts[1].comments, 2) + }) + + test('pass sideloaded data after applying group limit', async (assert) => { + class Comment extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public postId: number + + @column() + public body: string + } + + class Post extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + + @column() + public createdAt: Date + + @hasMany(() => Comment) + public comments: HasMany + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @hasMany(() => Post) + public posts: HasMany + } + + await db + .insertQuery() + .table('users') + .insert([{ username: 'virk' }, { username: 'nikk' }]) + + const [user0, user1] = await db.query().from('users') + + /** + * User 1 + */ + await db + .insertQuery() + .table('posts') + .multiInsert([ + { + user_id: user0.id, + title: 'Adonis 101', + created_at: new Date(), + }, + { + user_id: user0.id, + title: 'Adonis 102', + }, + { + user_id: user0.id, + title: 'Adonis 103', + }, + { + user_id: user0.id, + title: 'Adonis 104', + }, + { + user_id: user0.id, + title: 'Adonis 105', + created_at: new Date(), + }, + ]) + + /** + * User 2 + */ + await db + .insertQuery() + .table('posts') + .multiInsert([ + { + user_id: user1.id, + title: 'Lucid 101', + created_at: new Date(), + }, + { + user_id: user1.id, + title: 'Lucid 102', + }, + { + user_id: user1.id, + title: 'Lucid 103', + created_at: new Date(), + }, + { + user_id: user1.id, + title: 'Lucid 104', + created_at: new Date(), + }, + { + user_id: user1.id, + title: 'Lucid 105', + }, + ]) + + User.boot() + const posts = await Post.all() + + await Promise.all( + posts.map((post) => { + return post + .related('comments') + .createMany([{ body: 'Nice post' }, { body: 'Great post' }]) + }) + ) + + const users = await User.query().preload('posts', (query) => { + query.whereNotNull('created_at').groupLimit(2).preload('comments').sideload({ foo: 'bar' }) + }) + + assert.lengthOf(users, 2) + assert.lengthOf(users[0].posts, 2) + assert.lengthOf(users[0].posts[0].comments, 2) + assert.lengthOf(users[0].posts[1].comments, 2) + assert.deepEqual(users[0].posts[1].comments[0].$sideloaded, { foo: 'bar' }) + assert.deepEqual(users[0].posts[1].comments[1].$sideloaded, { foo: 'bar' }) + + assert.lengthOf(users[1].posts, 2) + assert.lengthOf(users[1].posts[0].comments, 2) + assert.lengthOf(users[1].posts[1].comments, 2) + assert.deepEqual(users[1].posts[1].comments[0].$sideloaded, { foo: 'bar' }) + assert.deepEqual(users[1].posts[1].comments[1].$sideloaded, { foo: 'bar' }) + }) }) }