Skip to content

Commit

Permalink
feat: implement belongsTo relationship
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Oct 1, 2019
1 parent 5371f88 commit 1df4d21
Show file tree
Hide file tree
Showing 5 changed files with 703 additions and 3 deletions.
2 changes: 1 addition & 1 deletion adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ declare module '@ioc:Adonis/Lucid/Model' {
column?: ThroughRelationDecoratorNode,
) => DecoratorFn

export type AvailableRelations = 'hasOne' | 'hasMany'
export type AvailableRelations = 'hasOne' | 'hasMany' | 'belongsTo'

/**
* Callback accepted by the preload method
Expand Down
6 changes: 5 additions & 1 deletion src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import {
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

import { proxyHandler } from './proxyHandler'
import { HasOne } from '../Relations/HasOne'
import { proxyHandler } from './proxyHandler'
import { HasMany } from '../Relations/HasMany'
import { BelongsTo } from '../Relations/BelongsTo'

function StaticImplements<T> () {
return (_t: T) => {}
Expand Down Expand Up @@ -269,6 +270,9 @@ export class BaseModel implements ModelContract {
case 'hasMany':
this.$relations.set(name, new HasMany(name, options, this))
break
case 'belongsTo':
this.$relations.set(name, new BelongsTo(name, options, this))
break
default:
throw new Error(`${type} relationship has not been implemented yet`)
}
Expand Down
2 changes: 1 addition & 1 deletion src/Orm/BaseModel/proxyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const DEFAULTS: {
} = {
hasOne: null,
hasMany: Object.freeze([]),
// belongsTo: null,
belongsTo: null,
// hasOneThrough: null,
// manyToMany: Object.freeze([]),
// hasManyThrough: Object.freeze([]),
Expand Down
213 changes: 213 additions & 0 deletions src/Orm/Relations/BelongsTo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* @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.
*/

/// <reference path="../../../adonis-typings/index.ts" />

import { Exception } from '@poppinss/utils'
import { camelCase, snakeCase, uniq } from 'lodash'

import {
ModelContract,
BaseRelationNode,
RelationContract,
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

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

export class BelongsTo implements RelationContract {
/**
* Relationship type
*/
public type = 'belongsTo' as const

/**
* The related model from which, we want to construct the relationship
*/
public relatedModel = this._options.relatedModel!

/**
* Local key to use for constructing the relationship
*/
public localKey: string

/**
* Adapter local key
*/
public localAdapterKey: string

/**
* Foreign key referenced by the related model
*/
public foreignKey: string

/**
* Adapter foreign key
*/
public foreignAdapterKey: string

/**
* Key to be used for serializing the relationship
*/
public serializeAs = this._options.serializeAs || snakeCase(this._relationName)

/**
* A flag to know if model keys valid for executing database queries or not
*/
private _isValid: boolean = false

constructor (
private _relationName: string,
private _options: BaseRelationNode,
private _model: ModelConstructorContract,
) {
this._validateOptions()
this._computeKeys()
}

/**
* Ensure that related model is defined, otherwise raise an exception, since
* a relationship cannot work with a single model.
*/
private _validateOptions () {
if (!this._options.relatedModel) {
throw new Exception(
'Related model reference is required to construct the relationship',
500,
'E_MISSING_RELATED_MODEL',
)
}
}

/**
* Compute keys
*/
private _computeKeys () {
if (this._isValid) {
return
}

this.localKey = this._options.localKey || this.relatedModel().$primaryKey
this.foreignKey = this._options.foreignKey || camelCase(
`${this.relatedModel().name}_${this.relatedModel().$primaryKey}`,
)

/**
* Validate computed keys to ensure they are valid
*/
this._validateKeys()

/**
* Keys for the adapter
*/
this.localAdapterKey = this.relatedModel().$getColumn(this.localKey)!.castAs
this.foreignAdapterKey = this._model.$getColumn(this.foreignKey)!.castAs
}

/**
* Validating the keys to ensure we are avoiding runtime `undefined` errors. We defer
* the keys validation, since they may be added after defining the relationship.
*/
private _validateKeys () {
const relationRef = `${this._model.name}.${this._relationName}`

if (!this._model.$hasColumn(this.foreignKey)) {
const ref = `${this._model.name}.${this.foreignKey}`
throw new Exception(
`${ref} required by ${relationRef} relation is missing`,
500,
'E_MISSING_RELATED_LOCAL_KEY',
)
}

if (!this.relatedModel().$hasColumn(this.localKey)) {
const ref = `${this.relatedModel().name}.${this.localKey}`
throw new Exception(
`${ref} required by ${relationRef} relation is missing`,
500,
'E_MISSING_RELATED_FOREIGN_KEY',
)
}

this._isValid = true
}

/**
* Raises exception when value for the foreign key is missing on the model instance. This will
* make the query fail
*/
protected $ensureValue (value: any) {
if (value === undefined) {
throw new Exception(
`Cannot preload ${this._relationName}, value of ${this._model.name}.${this.foreignKey} is undefined`,
500,
)
}

return value
}

/**
* Must be implemented by main class
*/
public getQuery (parent: ModelContract, client: QueryClientContract) {
const value = parent[this.foreignKey]

return this.relatedModel()
.query({ client })
.where(this.localAdapterKey, this.$ensureValue(value))
.limit(1)
}

/**
* Returns query for the relationship with applied constraints for
* eagerloading
*/
public getEagerQuery (parents: ModelContract[], client: QueryClientContract) {
const values = uniq(parents.map((parentInstance) => {
return this.$ensureValue(parentInstance[this.foreignKey])
}))

return this.relatedModel()
.query({ client })
.whereIn(this.localAdapterKey, values)
}

/**
* Sets the related model instance
*/
public setRelated (model: ModelContract, related?: ModelContract | ModelContract[] | null) {
if (!related) {
return
}

model.$setRelated(this._relationName as keyof typeof model, related)
}

/**
* Must be implemented by parent class
*/
public setRelatedMany (models: ModelContract[], related: ModelContract[]) {
/**
* Instead of looping over the model instances, we loop over the related model instances, since
* it can improve performance in some case. For example:
*
* - There are 10 parentInstances and we all of them to have one related instance, in
* this case we run 10 iterations.
* - There are 10 parentInstances and 8 of them have related instance, in this case we run 8
* iterations vs 10.
*/
related.forEach((one) => {
const relation = models.find((model) => model[this.foreignKey] === one[this.localKey])
if (relation) {
this.setRelated(relation, one)
}
})
}
}

0 comments on commit 1df4d21

Please sign in to comment.