Skip to content

Commit

Permalink
feat: add many to many relationship
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 12, 2020
1 parent 6593b38 commit 25ce9c7
Show file tree
Hide file tree
Showing 10 changed files with 1,004 additions and 15 deletions.
67 changes: 57 additions & 10 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ declare module '@ioc:Adonis/Lucid/Model' {
serializeAs?: string,
}

/**
* Shape of many to many relationship
*/
export interface ManyToManyRelationNode {
relatedModel: (() => ModelConstructorContract),
pivotTable?: string,
localKey?: string,
pivotForeignKey?: string,
relatedKey?: string,
pivotRelatedForeignKey?: string,
pivotColumns?: string[],
serializeAs?: string,
}

/**
* Shape of hasOneThrough relationship
*/
Expand Down Expand Up @@ -86,7 +100,9 @@ declare module '@ioc:Adonis/Lucid/Model' {
type DecoratorFn = (target, property) => void
type BaseRelationDecoratorNode = Omit<BaseRelationNode, 'relatedModel'>
type ThroughRelationDecoratorNode = Omit<ThroughRelationNode, 'relatedModel'>
type ManyToManyRelationDecoratorNode = Omit<ManyToManyRelationNode, 'relatedModel'>
type ModelExecuteableQueryBuilder = ModelQueryBuilderContract<any> & ExcutableQueryBuilderContract<any>
type ManyToManyExecutableQueryBuilder = ManyToManyQueryBuilderContract & ExcutableQueryBuilderContract<any>

/**
* Types for decorators
Expand All @@ -110,31 +126,33 @@ declare module '@ioc:Adonis/Lucid/Model' {
) => DecoratorFn

export type ManyToManyFn = (
model: BaseRelationNode['relatedModel'],
column?: BaseRelationDecoratorNode,
model: ManyToManyRelationNode['relatedModel'],
column?: ManyToManyRelationDecoratorNode,
) => DecoratorFn

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

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

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

/**
* Callback accepted by the preload method
* List of available relations
*/
export type PreloadCallback = (builder: ModelExecuteableQueryBuilder) => void
export type AvailableRelations = 'hasOne' | 'hasMany' | 'belongsTo' | 'manyToMany'

type ManyToManyPreloadCallback = (builder: ManyToManyExecutableQueryBuilder) => void
type BasePreloadCallback = (builder: ModelExecuteableQueryBuilder) => void
type PreloadCallback = ManyToManyPreloadCallback | BasePreloadCallback

/**
* Interface to be implemented by all relationship types
*/
export interface RelationContract {
export interface BaseRelationContract {
type: AvailableRelations
serializeAs: string
booted: boolean
Expand All @@ -146,6 +164,28 @@ declare module '@ioc:Adonis/Lucid/Model' {
setRelatedMany (models: ModelContract[], related: ModelContract[]): void
}

/**
* Shape of many to many relationship contract
*/
export interface ManyToManyRelationContract extends BaseRelationContract {
pivotTable: string
getQuery (model: ModelContract, client: QueryClientContract): ManyToManyExecutableQueryBuilder
getEagerQuery (models: ModelContract[], client: QueryClientContract): ManyToManyExecutableQueryBuilder
}

/**
* Relationships type
*/
export type RelationContract = BaseRelationContract | ManyToManyRelationContract

/**
* Shape of many to many query builder. It has few methods over the standard
* model query builder
*/
export interface ManyToManyQueryBuilderContract extends ModelQueryBuilderContract<any> {
pivotColumns (columns: string[]): this
}

/**
* Model query builder will have extras methods on top of Database query builder
*/
Expand Down Expand Up @@ -181,7 +221,14 @@ declare module '@ioc:Adonis/Lucid/Model' {
/**
* Define relationships to be preloaded
*/
preload (relation: string, callback?: PreloadCallback): this
preload<T extends 'manyToMany'> (
relation: string,
callback?: ManyToManyPreloadCallback,
): this
preload<T extends AvailableRelations> (
relation: string,
callback?: BasePreloadCallback,
): this
}

/**
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 @@ -34,6 +34,7 @@ import { HasOne } from '../Relations/HasOne'
import { proxyHandler } from './proxyHandler'
import { HasMany } from '../Relations/HasMany'
import { BelongsTo } from '../Relations/BelongsTo'
import { ManyToMany } from '../Relations/ManyToMany'

function StaticImplements<T> () {
return (_t: T) => {}
Expand Down Expand Up @@ -273,6 +274,9 @@ export class BaseModel implements ModelContract {
case 'belongsTo':
this.$relations.set(name, new BelongsTo(name, options, this))
break
case 'manyToMany':
this.$relations.set(name, new ManyToMany(name, options, this))
break
default:
throw new Error(`${type} relationship has not been implemented yet`)
}
Expand Down
7 changes: 6 additions & 1 deletion src/Orm/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
RelationContract,
ModelConstructorContract,
ModelQueryBuilderContract,
ManyToManyExecutableQueryBuilder,
} from '@ioc:Adonis/Lucid/Model'

import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'
Expand Down Expand Up @@ -100,6 +101,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
*/
private async _processRelation (models: ModelContract[], name: string) {
const relation = this._preloads[name]

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

Expand All @@ -112,7 +114,10 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
* Invoke callback when defined
*/
if (typeof (relation.callback) === 'function') {
relation.callback(query)
/**
* Type casting to superior type.
*/
relation.callback(query as ManyToManyExecutableQueryBuilder)
}

/**
Expand Down
7 changes: 5 additions & 2 deletions src/Orm/Relations/BelongsTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import { camelCase, snakeCase, uniq } from 'lodash'
import {
ModelContract,
BaseRelationNode,
RelationContract,
BaseRelationContract,
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

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

export class BelongsTo implements RelationContract {
/**
* Exposes the API to construct belongs to relationship.
*/
export class BelongsTo implements BaseRelationContract {
/**
* Relationship type
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Orm/Relations/HasOneOrMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { camelCase, snakeCase, uniq } from 'lodash'
import {
ModelContract,
BaseRelationNode,
RelationContract,
BaseRelationContract,
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

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

export abstract class HasOneOrMany implements RelationContract {
export abstract class HasOneOrMany implements BaseRelationContract {
/**
* Relationship type
*/
Expand Down
33 changes: 33 additions & 0 deletions src/Orm/Relations/ManyToMany/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* @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 knex from 'knex'
import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'
import { ManyToManyRelationContract, ManyToManyQueryBuilderContract } from '@ioc:Adonis/Lucid/Model'

import { ModelQueryBuilder } from '../../QueryBuilder'

export class ManyToManyQueryBuilder extends ModelQueryBuilder implements ManyToManyQueryBuilderContract {
constructor (
builder: knex.QueryBuilder,
private _relation: ManyToManyRelationContract,
client: QueryClientContract,
) {
super(builder, _relation.relatedModel(), client)
}

public pivotColumns (columns: string[]): this {
this.$knexBuilder.select(columns.map((column) => {
return `${this._relation.pivotTable}.${column} as pivot_${column}`
}))
return this
}
}
Loading

0 comments on commit 25ce9c7

Please sign in to comment.