Skip to content

Commit

Permalink
feat: add support for nested preloads
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Sep 26, 2019
1 parent 992f2fe commit 1ea384c
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 11 deletions.
7 changes: 4 additions & 3 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ declare module '@ioc:Adonis/Lucid/Model' {
* Interface to be implemented by all relationship types
*/
export interface RelationContract {
type: AvailableRelations,
serializeAs: string,
relatedModel (): ModelConstructorContract,
type: AvailableRelations
serializeAs: string
relatedModel (): ModelConstructorContract
preload (relation: string, callback?: (builder: ModelQueryBuilderContract<any>) => void): this
exec (
model: ModelContract | ModelContract[],
options?: ModelOptions,
Expand Down
28 changes: 20 additions & 8 deletions src/Orm/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
* A copy of defined preloads on the model instance
*/
private _preloads: {
relation: RelationContract,
callback?: (builder: ModelQueryBuilderContract<any>) => void,
}[] = []
[name: string]: {
relation: RelationContract,
callback?: (builder: ModelQueryBuilderContract<any>) => void,
},
} = {}

constructor (
builder: knex.QueryBuilder,
Expand All @@ -70,8 +72,9 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
this.options,
)

await Promise.all(this._preloads.map((one) => {
return one.relation.exec(modelInstances, this.options, one.callback)
await Promise.all(Object.keys(this._preloads).map((name) => {
const relation = this._preloads[name]
return relation.relation.exec(modelInstances, this.options, relation.callback)
}))
return modelInstances
}
Expand Down Expand Up @@ -107,16 +110,25 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
relationName: string,
callback?: (builder: ModelQueryBuilderContract<any>) => void,
): this {
const relation = this.model.$getRelation(relationName)
const relations = relationName.split('.')
const primary = relations.shift()!
const relation = this.model.$getRelation(primary)

/**
* Undefined relationship
*/
if (!relation) {
throw new Exception(`${relationName} is not defined as a relationship on ${this.model.name} model`)
throw new Exception(`${primary} is not defined as a relationship on ${this.model.name} model`)
}

const payload = this._preloads[primary] || { relation }
if (!relations.length) {
payload.callback = callback
} else {
payload.relation.preload(relations.join('.'), callback)
}

this._preloads.push({ relation, callback })
this._preloads[primary] = payload
return this
}

Expand Down
19 changes: 19 additions & 0 deletions src/Orm/Relations/HasOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export class HasOne implements RelationContract {
*/
private _isValid: boolean = false

/**
* Preloads to pass to the query builder
*/
private _preloads: { relationName: string, callback?: any }[] = []

constructor (
private _relationName: string,
private _options: Partial<BaseRelationNode>,
Expand Down Expand Up @@ -162,6 +167,14 @@ export class HasOne implements RelationContract {
return value
}

/**
* Takes preloads that we want to pass to the related query builder
*/
public preload (relationName: string, callback?: (builder: ModelQueryBuilderContract<any>) => void) {
this._preloads.push({ relationName, callback })
return this
}

/**
* Execute hasOne and set the relationship on model(s)
*/
Expand All @@ -171,6 +184,12 @@ export class HasOne implements RelationContract {
userCallback?: (builder: ModelQueryBuilderContract<any>) => void,
) {
const query = this.relatedModel().query(options)

/**
* Pass preloads to the query builder
*/
this._preloads.forEach(({ relationName, callback }) => query.preload(relationName, callback))

if (typeof (userCallback) === 'function') {
userCallback(query)
}
Expand Down
12 changes: 12 additions & 0 deletions test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ export async function setup () {
})
}

const hasIdentitiesTable = await db.schema.hasTable('identities')
if (!hasIdentitiesTable) {
await db.schema.createTable('identities', (table) => {
table.increments()
table.integer('profile_id')
table.string('identity_name')
table.timestamps()
})
}

await db.destroy()
}

Expand All @@ -128,6 +138,7 @@ export async function cleanup () {
const db = knex(getConfig())
await db.schema.dropTableIfExists('users')
await db.schema.dropTableIfExists('profiles')
await db.schema.dropTableIfExists('identities')
await db.destroy()
}

Expand All @@ -138,6 +149,7 @@ export async function resetTables () {
const db = knex(getConfig())
await db.table('users').truncate()
await db.table('profiles').truncate()
await db.table('identities').truncate()
}

/**
Expand Down
223 changes: 223 additions & 0 deletions test/model-has-one.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,227 @@ test.group('Model | Has one', (group) => {

assert.isNull(user!.profile)
})

test('preload nested relations', async (assert) => {
const BaseModel = getBaseModel(ormAdapter())

class Identity extends BaseModel {
@column({ primary: true })
public id: number

@column()
public profileId: number

@column()
public identityName: string
}

class Profile extends BaseModel {
@column({ primary: true })
public id: number

@column()
public userId: number

@column()
public displayName: string

@hasOne({ relatedModel: () => Identity })
public identity: Identity
}

class User extends BaseModel {
@column({ primary: true })
public id: number

@hasOne({ relatedModel: () => Profile })
public profile: Profile
}

const db = getDb()
await db.insertQuery().table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await db.insertQuery().table('profiles').insert([
{
user_id: 1,
display_name: 'virk',
},
{
user_id: 2,
display_name: 'nikk',
},
])

await db.insertQuery().table('identities').insert([
{
profile_id: 1,
identity_name: 'virk',
},
{
profile_id: 2,
identity_name: 'nikk',
},
])

User.$boot()

const user = await User.query()
.preload('profile.identity')
.where('username', 'virk')
.first()

assert.instanceOf(user!.profile, Profile)
assert.instanceOf(user!.profile!.identity, Identity)
})

test('preload nested relations with primary relation repeating twice', async (assert) => {
const BaseModel = getBaseModel(ormAdapter())

class Identity extends BaseModel {
@column({ primary: true })
public id: number

@column()
public profileId: number

@column()
public identityName: string
}

class Profile extends BaseModel {
@column({ primary: true })
public id: number

@column()
public userId: number

@column()
public displayName: string

@hasOne({ relatedModel: () => Identity })
public identity: Identity
}

class User extends BaseModel {
@column({ primary: true })
public id: number

@hasOne({ relatedModel: () => Profile })
public profile: Profile
}

const db = getDb()
await db.insertQuery().table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await db.insertQuery().table('profiles').insert([
{
user_id: 1,
display_name: 'virk',
},
{
user_id: 2,
display_name: 'nikk',
},
])

await db.insertQuery().table('identities').insert([
{
profile_id: 1,
identity_name: 'virk',
},
{
profile_id: 2,
identity_name: 'nikk',
},
])

User.$boot()

const query = User.query()
.preload('profile')
.preload('profile.identity')
.where('username', 'virk')

const user = await query.first()
assert.instanceOf(user!.profile, Profile)
assert.instanceOf(user!.profile!.identity, Identity)
assert.lengthOf(Object.keys(query['_preloads']), 1)
assert.property(query['_preloads'], 'profile')
assert.lengthOf(query['_preloads'].profile.relation._preloads, 1)
assert.equal(query['_preloads'].profile.relation._preloads[0].relationName, 'identity')
})

test('pass main query options down the chain', async (assert) => {
const BaseModel = getBaseModel(ormAdapter())

class Identity extends BaseModel {
@column({ primary: true })
public id: number

@column()
public profileId: number

@column()
public identityName: string
}

class Profile extends BaseModel {
@column({ primary: true })
public id: number

@column()
public userId: number

@column()
public displayName: string

@hasOne({ relatedModel: () => Identity })
public identity: Identity
}

class User extends BaseModel {
@column({ primary: true })
public id: number

@hasOne({ relatedModel: () => Profile })
public profile: Profile
}

const db = getDb()
await db.insertQuery().table('users').insert([{ username: 'virk' }, { username: 'nikk' }])
await db.insertQuery().table('profiles').insert([
{
user_id: 1,
display_name: 'virk',
},
{
user_id: 2,
display_name: 'nikk',
},
])

await db.insertQuery().table('identities').insert([
{
profile_id: 1,
identity_name: 'virk',
},
{
profile_id: 2,
identity_name: 'nikk',
},
])

User.$boot()

const query = User.query({ connection: 'secondary' })
.preload('profile')
.preload('profile.identity')
.where('username', 'virk')

const user = await query.first()
assert.instanceOf(user!.profile, Profile)
assert.instanceOf(user!.profile!.identity, Identity)

assert.equal(user!.$options!.connection, 'secondary')
assert.equal(user!.profile.$options!.connection, 'secondary')
assert.equal(user!.profile.identity.$options!.connection, 'secondary')
})
})

0 comments on commit 1ea384c

Please sign in to comment.