Skip to content

Commit

Permalink
feat: implement has many through
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Oct 2, 2019
1 parent c59d90e commit 4d0121f
Show file tree
Hide file tree
Showing 11 changed files with 725 additions and 15 deletions.
8 changes: 5 additions & 3 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ declare module '@ioc:Adonis/Lucid/Model' {
*/
export interface ThroughRelationNode extends BaseRelationNode {
throughModel: (() => ModelConstructorContract)
throughLocalKey?: string,
throughForeignKey?: string,
}

/**
Expand Down Expand Up @@ -137,13 +139,13 @@ declare module '@ioc:Adonis/Lucid/Model' {

export type HasManyThroughFn = (
model: ThroughRelationNode['relatedModel'],
column?: ThroughRelationDecoratorNode,
column: ThroughRelationDecoratorNode,
) => DecoratorFn

/**
* List of available relations
*/
export type AvailableRelations = 'hasOne' | 'hasMany' | 'belongsTo' | 'manyToMany'
export type AvailableRelations = 'hasOne' | 'hasMany' | 'belongsTo' | 'manyToMany' | 'hasManyThrough'

type ManyToManyPreloadCallback = (builder: ManyToManyExecutableQueryBuilder) => void
type BasePreloadCallback = (builder: ModelExecuteableQueryBuilder) => void
Expand All @@ -160,7 +162,7 @@ declare module '@ioc:Adonis/Lucid/Model' {
relatedModel (): ModelConstructorContract
getQuery (model: ModelContract, client: QueryClientContract): ModelExecuteableQueryBuilder
getEagerQuery (models: ModelContract[], client: QueryClientContract): ModelExecuteableQueryBuilder
setRelated (model: ModelContract, related?: ModelContract | null): void
setRelated (model: ModelContract, related?: ModelContract | ModelContract[] | null): void
setRelatedMany (models: ModelContract[], related: ModelContract[]): void
}

Expand Down
4 changes: 4 additions & 0 deletions src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { proxyHandler } from './proxyHandler'
import { HasMany } from '../Relations/HasMany'
import { BelongsTo } from '../Relations/BelongsTo'
import { ManyToMany } from '../Relations/ManyToMany'
import { HasManyThrough } from '../Relations/HasManyThrough'

function StaticImplements<T> () {
return (_t: T) => {}
Expand Down Expand Up @@ -277,6 +278,9 @@ export class BaseModel implements ModelContract {
case 'manyToMany':
this.$relations.set(name, new ManyToMany(name, options, this))
break
case 'hasManyThrough':
this.$relations.set(name, new HasManyThrough(name, options as ThroughRelationNode, 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 @@ -32,7 +32,7 @@ const DEFAULTS: {
belongsTo: null,
// hasOneThrough: null,
manyToMany: Object.freeze([]),
// hasManyThrough: Object.freeze([]),
hasManyThrough: Object.freeze([]),
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Orm/Decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const hasOneThrough: HasOneThroughFn = (relatedModel, relation?) => {
/**
* Define hasManyThrough relationship
*/
export const hasManyThrough: HasManyThroughFn = (relatedModel, relation?) => {
export const hasManyThrough: HasManyThroughFn = (relatedModel, relation) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Expand Down
4 changes: 2 additions & 2 deletions src/Orm/Relations/BelongsTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class BelongsTo implements BaseRelationContract {
*/
public getEagerQuery (parents: ModelContract[], client: QueryClientContract) {
const values = uniq(parents.map((parentInstance) => {
return this._ensureValue(parentInstance[this.foreignKey])
return this._ensureValue(parentInstance[this.localKey])
}))

return this.relatedModel()
Expand All @@ -184,7 +184,7 @@ export class BelongsTo implements BaseRelationContract {
/**
* Sets the related model instance
*/
public setRelated (model: ModelContract, related?: ModelContract | ModelContract[] | null) {
public setRelated (model: ModelContract, related?: ModelContract) {
if (!related) {
return
}
Expand Down
277 changes: 277 additions & 0 deletions src/Orm/Relations/HasManyThrough.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <[email protected]>
*
* 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,
ThroughRelationNode,
BaseRelationContract,
ModelConstructorContract,
ModelQueryBuilderContract,
} from '@ioc:Adonis/Lucid/Model'

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

export class HasManyThrough implements BaseRelationContract {
/**
* Relationship type
*/
public type: 'hasManyThrough'

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

/**
* The through model from which, we construct the through query
*/
public throughModel = this._options.throughModel

/**
* Local key on the parent model for constructing relationship
*/
public localKey: string

/**
* Adapter key for the defined `localKey`
*/
public localAdapterKey: string

/**
* Foreign key on the through model. NOTE: We do not have any direct
* relationship with the related model and hence our FK is on
* the through model
*/
public foreignKey: string

/**
* Adapter key for the defined `foreignKey`
*/
public foreignAdapterKey: string

/**
* The local (PK) on the through model.
*/
public throughLocalKey: string

/**
* Adapter key for the defined `throughLocalKey`
*/
public throughLocalAdapterKey: string

/**
* Foreign key on the `relatedModel`. This bounds the `throughModel` with
* the `relatedModel`.
*/
public throughForeignKey: string

/**
* Adapter key for the defined `throughForeignKey`
*/
public throughForeignAdapterKey: 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
*/
public booted: boolean = false

constructor (
private _relationName: string,
private _options: ThroughRelationNode,
private _model: ModelConstructorContract,
) {
this._ensureRelatedModel()
}

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

/**
* 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.localKey)) {
const ref = `${this._model.name}.${this.localKey}`
throw new Exception(
`${ref} required by ${relationRef} relation is missing`,
500,
'E_MISSING_RELATED_LOCAL_KEY',
)
}

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

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

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

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

return value
}

private _addSelect (query: ModelQueryBuilderContract<any>) {
query.select(
`${this.relatedModel().$table}.*`,
`${this.throughModel().$table}.${this.foreignAdapterKey} as through_${this.foreignAdapterKey}`,
)
}

private _addJoin (query: ModelQueryBuilderContract<any>) {
const throughTable = this.throughModel().$table
const relatedTable = this.relatedModel().$table

query.innerJoin(
`${throughTable}`,
`${throughTable}.${this.throughLocalAdapterKey}`,
`${relatedTable}.${this.throughForeignAdapterKey}`,
)
}

/**
* Compute keys
*/
public boot () {
if (this.booted) {
return
}

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

this.throughLocalKey = this._options.localKey || this.throughModel().$primaryKey // id (user)
this.throughForeignKey = this._options.throughForeignKey
|| camelCase(`${this.throughModel().name}_${this.throughModel().$primaryKey}`) // user_id (user)

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

/**
* Keys for the adapter
*/
this.localAdapterKey = this._model.$getColumn(this.localKey)!.castAs
this.foreignAdapterKey = this.throughModel().$getColumn(this.foreignKey)!.castAs
this.throughLocalAdapterKey = this.throughModel().$getColumn(this.throughLocalKey)!.castAs
this.throughForeignAdapterKey = this.relatedModel().$getColumn(this.throughForeignKey)!.castAs
this.booted = true
}

/**
* 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.localKey])
}))

const throughTable = this.throughModel().$table
const query = this.relatedModel().query({ client })

this._addJoin(query)
this._addSelect(query)

return query.whereIn(`${throughTable}.${this.foreignAdapterKey}`, values)
}

/**
* Returns query for the relationship with applied constraints
*/
public getQuery (parent: ModelContract, client: QueryClientContract) {
const value = parent[this.localKey]
const throughTable = this.throughModel().$table
const query = this.relatedModel().query({ client })

this._addJoin(query)
this._addSelect(query)

return query.where(`${throughTable}.${this.foreignAdapterKey}`, this._ensureValue(value))
}

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

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

/**
* Set many related instances
*/
public setRelatedMany (parents: ModelContract[], related: ModelContract[]) {
parents.forEach((parent) => {
const relation = related.filter((model) => {
return model.$extras[`through_${this.foreignAdapterKey}`] === parent[this.localKey]
})
this.setRelated(parent, relation)
})
}
}
2 changes: 1 addition & 1 deletion src/Orm/Relations/HasOneOrMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export abstract class HasOneOrMany implements BaseRelationContract {
/**
* Sets the related model instance
*/
public setRelated (parent: ModelContract, related?: ModelContract | ModelContract[] | null) {
public setRelated (parent: ModelContract, related?: ModelContract | ModelContract[]) {
if (!related) {
return
}
Expand Down
7 changes: 2 additions & 5 deletions src/Orm/Relations/ManyToMany/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export class ManyToMany implements ManyToManyRelationContract {
/**
* Sets the related model instance
*/
public setRelated (model: ModelContract, related?: ModelContract | ModelContract[] | null) {
public setRelated (model: ModelContract, related?: ModelContract[] | null) {
if (!related) {
return
}
Expand All @@ -270,10 +270,7 @@ export class ManyToMany implements ManyToManyRelationContract {
const relation = related.filter((model) => {
return parent[this.localKey] === model.$extras[this.pivotForeignKeyAlias]
})

if (relation) {
this.setRelated(parent, relation)
}
this.setRelated(parent, relation)
})
}
}
Loading

0 comments on commit 4d0121f

Please sign in to comment.