Skip to content

Commit

Permalink
feat: adding support to save related models
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 12, 2020
1 parent a7aa2d0 commit f2f385c
Show file tree
Hide file tree
Showing 22 changed files with 2,992 additions and 712 deletions.
103 changes: 62 additions & 41 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ declare module '@ioc:Adonis/Lucid/Model' {
* Lookup map required for related method
*/
type RelationsQueryBuildersMap<T> = {
'unknown': ModelExecuteableQueryBuilder,
'unknown': BaseRelationQueryBuilderContract<T> & ExcutableQueryBuilderContract<T[]>,
'hasOne': HasOneQueryBuilderContract<T> & ExcutableQueryBuilderContract<T[]>,
'hasMany': HasManyQueryBuilderContract<T> & ExcutableQueryBuilderContract<T[]>,
'belongsTo': BelongsToQueryBuilderContract<T> & ExcutableQueryBuilderContract<T[]>,
Expand Down Expand Up @@ -204,8 +204,11 @@ declare module '@ioc:Adonis/Lucid/Model' {
*/
export interface RelationContract {
type: AvailableRelations
relationName: string
serializeAs: string
booted: boolean
model: ModelConstructorContract

boot (): void
relatedModel (): ModelConstructorContract

Expand All @@ -224,18 +227,11 @@ declare module '@ioc:Adonis/Lucid/Model' {
}

/**
* A union of relation relations query builders
* Base query builder for all relations
*/
type RelationQueryBuilderContract<T extends any = ModelContract> = BelongsToQueryBuilderContract<T> |
HasOneQueryBuilderContract<T> |
HasManyQueryBuilderContract<T> |
ManyToManyQueryBuilderContract<T> |
HasManyThroughQueryBuilderContract<T>
export interface BaseRelationQueryBuilderContract<T> extends ModelQueryBuilderContract<any> {
applyConstraints (): this

/**
* Shae of has belongs to query builder contract
*/
export interface BelongsToQueryBuilderContract<T> extends ModelQueryBuilderContract<any> {
/**
* Execute and get first result
*/
Expand All @@ -245,36 +241,52 @@ declare module '@ioc:Adonis/Lucid/Model' {
* Return the first matching row or fail
*/
firstOrFail (): Promise<T>
}

/**
* Shae of has one relationship query builder
*/
export interface HasOneQueryBuilderContract<T> extends ModelQueryBuilderContract<any> {
/**
* Execute and get first result
* Save the related model.
*/
first (): Promise<T | null>
save (model: T, wrapInTransaction?: boolean): Promise<void>

/**
* Return the first matching row or fail
* Save the related model.
*/
firstOrFail (): Promise<T>
saveMany (model: T[], wrapInTransaction?: boolean): Promise<void>
}

/**
* Shae of has many relationship query builder
* A union of relation relations query builders
*/
export interface HasManyQueryBuilderContract<T> extends ModelQueryBuilderContract<any> {
type RelationQueryBuilderContract<T extends any = ModelContract> = BelongsToQueryBuilderContract<T> |
HasOneQueryBuilderContract<T> |
HasManyQueryBuilderContract<T> |
ManyToManyQueryBuilderContract<T> |
HasManyThroughQueryBuilderContract<T>

/**
* Shae of has belongs to query builder contract
*/
export interface BelongsToQueryBuilderContract<T> extends BaseRelationQueryBuilderContract<T> {
/**
* Execute and get first result
* Associate related model.
*/
first (): Promise<T | null>
associate (model: T, wrapInTransaction?: boolean): Promise<void>

/**
* Return the first matching row or fail
* Dissociate all relationships.
*/
firstOrFail (): Promise<T>
dissociate (): Promise<void>
}

/**
* Shae of has one relationship query builder
*/
export interface HasOneQueryBuilderContract<T> extends BaseRelationQueryBuilderContract<T> {
}

/**
* Shae of has many relationship query builder
*/
export interface HasManyQueryBuilderContract<T> extends BaseRelationQueryBuilderContract<T> {
}

/**
Expand All @@ -299,7 +311,7 @@ declare module '@ioc:Adonis/Lucid/Model' {
* Shape of many to many query builder. It has few methods over the standard
* model query builder
*/
export interface ManyToManyQueryBuilderContract<T> extends ModelQueryBuilderContract<any> {
export interface ManyToManyQueryBuilderContract<T> extends BaseRelationQueryBuilderContract<T> {
pivotColumns (columns: string[]): this

wherePivot: WherePivot<this>
Expand All @@ -319,29 +331,38 @@ declare module '@ioc:Adonis/Lucid/Model' {
andWhereNotInPivot: WhereInPivot<this>

/**
* Execute and get first result
* Save related model
*/
first (): Promise<T | null>
save (model: T, wrapInTransaction?: boolean, checkExisting?: boolean): Promise<void>

/**
* Return the first matching row or fail
* Save related many
*/
firstOrFail (): Promise<T>
saveMany (model: T[], wrapInTransaction?: boolean, checkExisting?: boolean): Promise<void>

/**
* Attach related
*/
attach (
ids: (string | number)[] | { [key: string]: any },
checkExisting?: boolean,
): Promise<void>

// /**
// * Attach related
// */
// detach (ids: any[]): Promise<void>

// /**
// * Attach related
// */
// sync (ids: any[], detach: boolean): Promise<void>
}

/**
* Shae of has many through relationship query builder
*/
export interface HasManyThroughQueryBuilderContract<T> extends ModelQueryBuilderContract<any> {
/**
* Execute and get first result
*/
first (): Promise<T | null>

/**
* Return the first matching row or fail
*/
firstOrFail (): Promise<T>
export interface HasManyThroughQueryBuilderContract<T> extends BaseRelationQueryBuilderContract<T> {
}

/**
Expand Down
4 changes: 3 additions & 1 deletion example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ class User extends BaseModel {
}

const user = new User()
user.related('profile').where('username', 'virk').exec()
user.related<'hasOne', 'profile'>('profile').save(new Profile())

user.profile = new Profile()
4 changes: 2 additions & 2 deletions src/Database/QueryBuilder/Chainable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export abstract class Chainable implements ChainableContract {
/**
* Define columns for selection
*/
public select (): this {
this.$knexBuilder.select(...arguments)
public select (...args: any): this {
this.$knexBuilder.select(...args)
return this
}

Expand Down
121 changes: 121 additions & 0 deletions src/Orm/Relations/Base/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* @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.
*/
/*
* @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 {
ModelContract,
RelationContract,
BaseRelationQueryBuilderContract,
} from '@ioc:Adonis/Lucid/Model'

import { DBQueryCallback } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder'
import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database'

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

/**
* Exposes the API for interacting with has many relationship
*/
export abstract class BaseRelationQueryBuilder
extends ModelQueryBuilder
implements BaseRelationQueryBuilderContract<any>
{
protected $appliedConstraints: boolean = false

constructor (
builder: knex.QueryBuilder,
private _baseRelation: RelationContract,
client: QueryClientContract,
queryCallback: DBQueryCallback,
) {
super(builder, _baseRelation.relatedModel(), client, queryCallback)
}

/**
* Applies constraints for `select`, `update` and `delete` queries. The
* inserts are not allowed directly and one must use `save` method
* instead.
*/
public abstract applyConstraints (): this

/**
* Adds neccessary where clause to the query to perform the select
*/
public beforeExecute () {
this.applyConstraints()
}

/**
* Read value for a key on a model instance, in reference to the
* relationship operations
*/
protected $getRelatedValue (model: ModelContract, key: string, action = 'preload') {
return getValue(model, key, this._baseRelation, action)
}

/**
* Persists related model instance by setting the FK
*/
protected async $persist<T extends ModelContract, V extends ModelContract> (
parent: T,
related: V | V[],
cb: (parent: T, related: V) => void,
) {
await parent.save()

related = Array.isArray(related) ? related : [related]
await Promise.all(related.map((relation) => {
/**
* Copying options and trx to make sure relation is using
* the same options from the parent model
*/
relation.$trx = parent.$trx
relation.$options = parent.$options
cb(parent, relation)

return relation.save()
}))
}

/**
* Persists related model instance inside a transaction. Transaction is
* created only when parent model is not persisted and user has not
* disabled transactions as well.
*/
protected async $persistInTrx<T extends ModelContract, V extends ModelContract> (
parent: T,
related: V | V[],
trx: TransactionClientContract,
cb: (parent: T, related: V) => void,
) {
try {
parent.$trx = trx
await this.$persist(parent, related, cb)
await trx.commit()
} catch (error) {
await trx.rollback()
throw error
}
}

public abstract async save (model: ModelContract, wrapInTransaction?: boolean): Promise<void>
public abstract async saveMany (model: ModelContract[], wrapInTransaction?: boolean): Promise<void>
}
Loading

0 comments on commit f2f385c

Please sign in to comment.