Skip to content

Commit

Permalink
refactor: Abstract preloading logic to it's own class
Browse files Browse the repository at this point in the history
Because, we want to use the same preloading concepts on a single model
as well
  • Loading branch information
thetutlage committed Oct 3, 2019
1 parent 4d0121f commit 61e6e04
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 84 deletions.
1 change: 0 additions & 1 deletion src/Orm/BaseModel/proxyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const DEFAULTS: {
hasOne: null,
hasMany: Object.freeze([]),
belongsTo: null,
// hasOneThrough: null,
manyToMany: Object.freeze([]),
hasManyThrough: Object.freeze([]),
}
Expand Down
209 changes: 209 additions & 0 deletions src/Orm/Preloader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Exception } from '@poppinss/utils'
import {
ModelContract,
PreloadCallback,
RelationContract,
ModelConstructorContract,
ManyToManyExecutableQueryBuilder,
} from '@ioc:Adonis/Lucid/Model'

import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'

type PreloadNode = {
relation: RelationContract,
callback?: PreloadCallback,
children: { relationName: string, callback?: PreloadCallback }[],
}

/**
* Exposes the API to define and preload relationships in reference to
* a model
*/
export class Preloader {
/**
* Registered preloads
*/
private _preloads: { [key: string]: PreloadNode } = {}

constructor (private _model: ModelConstructorContract) {
}

/**
* Returns the preload node for the a given relationship name
*/
private _getPreloadedRelation (name: string) {
const relation = this._preloads[name]
if (!relation) {
throw new Exception(`Cannot process unregistered relationship ${name}`, 500)
}

relation.relation.boot()
return relation
}

/**
* Execute the related query
*/
private async _executeQuery (
query: ManyToManyExecutableQueryBuilder,
preload: PreloadNode,
): Promise<ModelContract[]> {
/**
* Pass nested preloads
*/
preload.children.forEach(({ relationName, callback }) => query.preload(relationName, callback))

/**
* Invoke callback when defined
*/
if (typeof (preload.callback) === 'function') {
/**
* Type casting to superior type.
*/
preload.callback(query as ManyToManyExecutableQueryBuilder)
}

/**
* Execute query
*/
return query.exec()
}

/**
* Parses the relation name for finding nested relations. The
* children `relationName` must be further parsed until
* the last segment
*/
public parseRelationName (relationName: string) {
const relations = relationName.split('.')
const primary = relations.shift()!
const relation = this._model.$getRelation(primary)

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

return {
primary,
relation,
children: relations.length ? { relationName: relations.join('') } : null,
}
}

/**
* Define relationship to be preloaded
*/
public preload (relationName: string, userCallback?: PreloadCallback) {
const { primary, relation, children } = this.parseRelationName(relationName)

const payload = this._preloads[primary] || { relation, children: [] }
if (children) {
payload.children.push(Object.assign(children, { callback: userCallback }))
} else {
payload.callback = userCallback
}

this._preloads[primary] = payload
return this
}

/**
* Process a single relationship for a single parent model
*/
public async processForOne (
name: string,
model: ModelContract,
client: QueryClientContract,
): Promise<void> {
/**
* Get the relation
*/
const relation = this._getPreloadedRelation(name)

/**
* Pull query for a single parent model instance
*/
const query = relation.relation.getQuery(model, client)

/**
* Execute the query
*/
const result = await this._executeQuery(query as ManyToManyExecutableQueryBuilder, relation)

/**
* Set only one when relationship is hasOne or belongsTo
*/
if (['hasOne', 'belongsTo'].includes(relation.relation.type)) {
relation.relation.setRelated(model, result[0])
return
}

/**
* Set relationships on model
*/
relation.relation.setRelated(model, result)
}

/**
* Process a single relationship for a many parent models
*/
public async processForMany (
name: string,
models: ModelContract[],
client: QueryClientContract,
): Promise<void> {
/**
* Get the relation
*/
const relation = this._getPreloadedRelation(name)

/**
* Pull query for a single parent model instance
*/
const query = relation.relation.getEagerQuery(models, client)

/**
* Execute the query
*/
const result = await this._executeQuery(query as ManyToManyExecutableQueryBuilder, relation)

/**
* Set relationships on model
*/
relation.relation.setRelatedMany(models, result)
}

/**
* Process all preloaded for many parent models
*/
public async processAllForMany (models: ModelContract[], client: QueryClientContract): Promise<void> {
await Promise.all(Object.keys(this._preloads).map((name) => {
return this.processForMany(name, models, client)
}))
}

/**
* Processes all relationships for one parent model
*/
public async processAllForOne (model: ModelContract, client: QueryClientContract): Promise<void> {
await Promise.all(Object.keys(this._preloads).map((name) => {
return this.processForOne(name, model, client)
}))
}
}
88 changes: 5 additions & 83 deletions src/Orm/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@ import { Exception } from '@poppinss/utils'

import {
ModelOptions,
ModelContract,
PreloadCallback,
RelationContract,
ModelConstructorContract,
ModelQueryBuilderContract,
ManyToManyExecutableQueryBuilder,
} from '@ioc:Adonis/Lucid/Model'

import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'
import { Chainable } from '../../Database/QueryBuilder/Chainable'

import { Preloader } from '../Preloader'
import { Executable, ExecutableConstructor } from '../../Traits/Executable'

/**
Expand All @@ -42,13 +41,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
/**
* A copy of defined preloads on the model instance
*/
private _preloads: {
[name: string]: {
relation: RelationContract,
callback?: PreloadCallback,
children: { relationName: string, callback?: PreloadCallback }[],
},
} = {}
private _preloader = new Preloader(this.model)

/**
* Options that must be passed to all new model instances
Expand All @@ -72,65 +65,6 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
builder.table(model.$table)
}

/**
* Parses the relation name string for nested relations. Nested relations
* can be defined using the dot notation.
*/
private _parseRelationName (relationName: string, callback?: PreloadCallback) {
const relations = relationName.split('.')
const primary = relations.shift()!
const relation = this.model.$getRelation(primary)

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

return {
primary,
relation,
children: relations.length ? { relationName: relations.join(''), callback } : null,
callback: relations.length ? null : callback,
}
}

/**
* Process preloaded relationship
*/
private async _processRelation (models: ModelContract[], name: string) {
const relation = this._preloads[name]

relation.relation.boot()
const query = relation.relation.getEagerQuery(models, this.client)

/**
* Pass nested preloads
*/
relation.children.forEach(({ relationName, callback }) => query.preload(relationName, callback))

/**
* Invoke callback when defined
*/
if (typeof (relation.callback) === 'function') {
/**
* Type casting to superior type.
*/
relation.callback(query as ManyToManyExecutableQueryBuilder)
}

/**
* Execute query
*/
const result = await query.exec()

/**
* Set relationships on models
*/
relation.relation.setRelatedMany(models, result)
}

/**
* Wraps the query result to model instances. This method is invoked by the
* Executable trait.
Expand All @@ -142,10 +76,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
this._options,
)

await Promise.all(Object.keys(this._preloads).map((name) => {
return this._processRelation(modelInstances, name)
}))

await this._preloader.processAllForMany(modelInstances, this.client)
return modelInstances
}

Expand Down Expand Up @@ -190,16 +121,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
* Define a relationship to be preloaded
*/
public preload (relationName: string, userCallback?: PreloadCallback): this {
const { primary, relation, children, callback } = this._parseRelationName(relationName, userCallback)

const payload = this._preloads[primary] || { relation, children: [] }
if (children) {
payload.children.push(children)
} else {
payload.callback = callback!
}

this._preloads[primary] = payload
this._preloader.preload(relationName, userCallback)
return this
}

Expand Down

0 comments on commit 61e6e04

Please sign in to comment.