Skip to content

Commit

Permalink
feat: add support for preloading using model instance
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Oct 3, 2019
1 parent 726762d commit c6e2f31
Show file tree
Hide file tree
Showing 9 changed files with 638 additions and 9 deletions.
43 changes: 41 additions & 2 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,32 @@ declare module '@ioc:Adonis/Lucid/Model' {
pivotColumns (columns: string[]): this
}

/**
* Shape of the preloader to preload relationships
*/
export interface PreloaderContract {
parseRelationName (relationName: string): {
primary: string,
relation: RelationContract,
children: { relationName: string } | null,
}

processForOne (name: string, model: ModelContract, client: QueryClientContract): Promise<void>
processForMany (name: string, models: ModelContract[], client: QueryClientContract): Promise<void>
processAllForOne (models: ModelContract, client: QueryClientContract): Promise<void>
processAllForMany (models: ModelContract[], client: QueryClientContract): Promise<void>

preload<T extends 'manyToMany'> (
relation: string,
callback?: ManyToManyPreloadCallback,
): this

preload<T extends AvailableRelations> (
relation: string,
callback?: BasePreloadCallback,
): this
}

/**
* Model query builder will have extras methods on top of Database query builder
*/
Expand Down Expand Up @@ -291,8 +317,9 @@ declare module '@ioc:Adonis/Lucid/Model' {
/**
* Read/write realtionships
*/
$getRelated<K extends keyof this> (key: K, defaultValue?: any): this[K]
$setRelated<K extends keyof this> (key: K, result: this[K]): void
$hasRelated (key: string): boolean
$setRelated (key: string, result: ModelContract | ModelContract[]): void
$getRelated (key: string, defaultValue?: any): ModelContract

/**
* Consume the adapter result and hydrate the model
Expand All @@ -302,6 +329,18 @@ declare module '@ioc:Adonis/Lucid/Model' {
fill (value: ModelObject): void
merge (value: ModelObject): void

preload<T extends 'manyToMany'> (
relation: string,
callback?: ManyToManyPreloadCallback,
): Promise<void>

preload<T extends AvailableRelations> (
relation: string,
callback?: BasePreloadCallback,
): Promise<void>

preload (callback: (preloader: PreloaderContract) => void): Promise<void>

save (): Promise<void>
delete (): Promise<void>
serialize (): ModelObject
Expand Down
45 changes: 40 additions & 5 deletions src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ComputedNode,
ModelContract,
AdapterContract,
PreloadCallback,
BaseRelationNode,
RelationContract,
AvailableRelations,
Expand All @@ -30,6 +31,7 @@ import {
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

import { Preloader } from '../Preloader'
import { HasOne } from '../Relations/HasOne'
import { proxyHandler } from './proxyHandler'
import { HasMany } from '../Relations/HasMany'
Expand Down Expand Up @@ -708,15 +710,26 @@ export class BaseModel implements ModelContract {
/**
* Returns the related model or default value when model is missing
*/
public $getRelated<K extends keyof this> (key: K, defaultValue: any): any {
return this.$preloaded[key as string] || defaultValue
public $getRelated (key: string, defaultValue: any): any {
if (this.$hasRelated(key)) {
return this.$preloaded[key as string]
}

return defaultValue
}

/**
* A boolean to know if relationship has been preloaded or not
*/
public $hasRelated (key: string): boolean {
return this.$preloaded[key as string] !== undefined
}

/**
* Sets the related data on the model instance. The method internally handles
* `one to one` or `many` relations
*/
public $setRelated<K extends keyof this> (key: K, models: this[K]) {
public $setRelated (key: string, models: ModelContract | ModelContract[]) {
const Model = this.constructor as typeof BaseModel
const relation = Model.$relations.get(key as string)

Expand All @@ -730,9 +743,12 @@ export class BaseModel implements ModelContract {
/**
* Create multiple for `hasMany` and one for `belongsTo` and `hasOne`
*/
if (['hasMany', 'manyToMany', 'hasManyThrough'].includes(relation.type)) {
const manyRelationships = ['hasMany', 'manyToMany', 'hasManyThrough']
if (manyRelationships.includes(relation.type)) {
if (!Array.isArray(models)) {
throw new Error('Pass array please')
throw new Exception(`
$setRelated accepts an array of related models for ${manyRelationships.join(',')} relationships,
`)
}
this.$preloaded[key as string] = models
} else {
Expand Down Expand Up @@ -831,6 +847,25 @@ export class BaseModel implements ModelContract {
}
}

/**
* Preloads one or more relationships for the current model
*/
public async preload (
relationName: string | ((preloader: Preloader) => void),
callback?: PreloadCallback,
) {
const constructor = this.constructor as ModelConstructorContract
const preloader = new Preloader(constructor)

if (typeof (relationName) === 'function') {
relationName(preloader)
} else {
preloader.preload(relationName, callback)
}

await preloader.processAllForOne(this, constructor.query(this.$getOptions()).client)
}

/**
* Perform save on the model instance to commit mutations.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/Orm/Preloader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ModelContract,
PreloadCallback,
RelationContract,
PreloaderContract,
ModelConstructorContract,
ManyToManyExecutableQueryBuilder,
} from '@ioc:Adonis/Lucid/Model'
Expand All @@ -28,7 +29,7 @@ type PreloadNode = {
* Exposes the API to define and preload relationships in reference to
* a model
*/
export class Preloader {
export class Preloader implements PreloaderContract {
/**
* Registered preloads
*/
Expand Down
Binary file modified test-helpers/tmp/db.sqlite
Binary file not shown.
107 changes: 107 additions & 0 deletions test/orm/model-belongs-to.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,47 @@ test.group('Model | Belongs To', (group) => {
assert.equal(profiles[2].user.id, 2)
})

test('preload using model instance', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number
}

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

@column()
public userId: number

@column()
public displayName: string

@belongsTo(() => User)
public user: User
}

await db.insertQuery().table('users').insert([{ username: 'virk' }])

const users = await db.query().from('users')
await db.insertQuery().table('profiles').insert([
{
user_id: users[0].id,
display_name: 'virk',
},
{
user_id: users[0].id,
display_name: 'virk',
},
])

const profile = await Profile.findOrFail(1)
await profile.preload('user')

assert.instanceOf(profile.user, User)
assert.equal(profile.user.id, profile.userId)
})

test('raise exception when foreign key is not selected', async (assert) => {
assert.plan(1)
class User extends BaseModel {
Expand Down Expand Up @@ -541,6 +582,72 @@ test.group('Model | Belongs To', (group) => {
assert.equal(query['_preloader']['_preloads'].profile.children[0].relationName, 'user')
})

test('preload nested relations using model instance', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number
}

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

@column()
public userId: number

@column()
public displayName: string

@belongsTo(() => User)
public user: User
}

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

@column()
public profileId: number

@column()
public identityName: string

@belongsTo(() => Profile)
public profile: Profile
}

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',
},
])

const identity = await Identity.query().firstOrFail()
await identity.preload((preloader) => {
preloader.preload('profile').preload('profile.user')
})

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

test('pass main query options down the chain', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
Expand Down
63 changes: 63 additions & 0 deletions test/orm/model-has-many-through.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,67 @@ test.group('Model | Has Many Through', (group) => {
assert.equal(countries[1].posts[0].title, 'Adonis5')
assert.equal(countries[1].posts[0].$extras.through_country_id, 2)
})

test('preload many relationships using model instance', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public countryId: number
}
User.$boot()

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

@column()
public userId: number

@column()
public title: string
}
Post.$boot()

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

@hasManyThrough([() => Post, () => User])
public posts: Post[]
}
Country.$boot()

await db.insertQuery().table('countries').insert([{ name: 'India' }, { name: 'USA' }])

await db.insertQuery().table('users').insert([
{ username: 'virk', country_id: 1 },
{ username: 'nikk', country_id: 2 },
])

await db.insertQuery().table('posts').insert([
{ title: 'Adonis 101', user_id: 1 },
{ title: 'Lucid 101', user_id: 1 },
{ title: 'Adonis5', user_id: 2 },
])

const countries = await Country.query().orderBy('id', 'asc')
assert.lengthOf(countries, 2)

await countries[0].preload('posts')
await countries[1].preload('posts')

assert.lengthOf(countries[0].posts, 2)
assert.lengthOf(countries[1].posts, 1)

assert.equal(countries[0].posts[0].title, 'Adonis 101')
assert.equal(countries[0].posts[0].$extras.through_country_id, 1)

assert.equal(countries[0].posts[1].title, 'Lucid 101')
assert.equal(countries[0].posts[1].$extras.through_country_id, 1)

assert.equal(countries[1].posts[0].title, 'Adonis5')
assert.equal(countries[1].posts[0].$extras.through_country_id, 2)
})
})

0 comments on commit c6e2f31

Please sign in to comment.