Skip to content

Commit

Permalink
feat: add updateOrCreate to hasOne and hasMany
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Dec 14, 2019
1 parent 90e6e02 commit 61e7d7c
Show file tree
Hide file tree
Showing 10 changed files with 758 additions and 41 deletions.
19 changes: 14 additions & 5 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,15 @@ declare module '@ioc:Adonis/Lucid/Model' {
* Create many of the related model instance
*/
createMany (values: ModelObject[], wrapInTransaction?: boolean): Promise<T[]>

/**
* Update the relationship or create a new one
*/
updateOrCreate (
search: ModelObject,
updatePayload: ModelObject,
wrapInTransaction?: boolean,
): Promise<T>
}

/**
Expand Down Expand Up @@ -715,20 +724,20 @@ declare module '@ioc:Adonis/Lucid/Model' {
): Promise<InstanceType<T>[]>

/**
* Returns the first row or save it to the database
* Returns the first row or create a new instance of model without
* persisting it
*/
firstOrCreate<T extends ModelConstructorContract> (
firstOrNew<T extends ModelConstructorContract> (
this: T,
search: any,
savePayload?: any,
options?: ModelAdapterOptions,
): Promise<InstanceType<T>>

/**
* Returns the first row or create a new instance of model without
* persisting it
* Returns the first row or save it to the database
*/
firstOrNew<T extends ModelConstructorContract> (
firstOrCreate<T extends ModelConstructorContract> (
this: T,
search: any,
savePayload?: any,
Expand Down
24 changes: 12 additions & 12 deletions src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,15 +416,19 @@ export class BaseModel implements ModelContract {
/**
* Find model instance using a key/value pair
*/
public static async firstOrCreate<T extends ModelConstructorContract> (
public static async firstOrNew<T extends ModelConstructorContract> (
this: T,
search: any,
savePayload?: any,
options?: ModelAdapterOptions,
) {
const row = await this.firstOrNew(search, savePayload, options)
if (!row.$persisted) {
await row.save()
const query = this.query(options)
let row = await query.where(this.$mapKeysToCastKeys(search)).first()

if (!row) {
row = new this() as InstanceType<T>
row.fill(Object.assign({}, search, savePayload))
row.$setOptionsAndTrx(query.clientOptions)
}

return row
Expand All @@ -433,19 +437,15 @@ export class BaseModel implements ModelContract {
/**
* Find model instance using a key/value pair
*/
public static async firstOrNew<T extends ModelConstructorContract> (
public static async firstOrCreate<T extends ModelConstructorContract> (
this: T,
search: any,
savePayload?: any,
options?: ModelAdapterOptions,
) {
const query = this.query(options)
let row = await query.where(this.$mapKeysToCastKeys(search)).first()

if (!row) {
row = new this() as InstanceType<T>
row.fill(Object.assign({}, search, savePayload))
row.$setOptionsAndTrx(query.clientOptions)
const row = await this.firstOrNew(search, savePayload, options)
if (!row.$persisted) {
await row.save()
}

return row
Expand Down
73 changes: 69 additions & 4 deletions src/Orm/Relations/Base/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ModelObject,
ModelContract,
RelationContract,
ModelAdapterOptions,
BaseRelationQueryBuilderContract,
} from '@ioc:Adonis/Lucid/Model'

Expand Down Expand Up @@ -124,6 +125,49 @@ export abstract class BaseRelationQueryBuilder
}
}

/**
* Update relationship or create a new one.
*/
protected async $updateOrCreate<T extends ModelContract> (
parent: T,
cb: (parent: T) => { searchPayload: ModelObject, updatePayload: ModelObject },
) {
await parent.save()
const { searchPayload, updatePayload } = cb(parent)

/**
* Passing down options of the parent model to the
* related model
*/
const options: ModelAdapterOptions = parent.$options || {}
if (parent.$trx) {
options.client = parent.$trx
}

return this._baseRelation
.relatedModel()
.updateOrCreate(searchPayload, updatePayload, options)
}

/**
* Update relationship inside a managed transaction
*/
protected async $updateOrCreateInTrx<T extends ModelContract> (
parent: T,
cb: (parent: T) => { searchPayload: ModelObject, updatePayload: ModelObject },
trx: TransactionClientContract,
) {
try {
parent.$trx = trx
const related = await this.$updateOrCreate(parent, cb)
await trx.commit()
return related
} catch (error) {
await trx.rollback()
throw error
}
}

/**
* Returns the query action
*/
Expand All @@ -138,8 +182,29 @@ export abstract class BaseRelationQueryBuilder
return action
}

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

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

public abstract async create (
model: ModelObject,
wrapInTransaction?: boolean,
): Promise<ModelContract>

public abstract async createMany (
model: ModelObject[],
wrapInTransaction?: boolean,
): Promise<ModelContract[]>

public abstract async updateOrCreate (
search: ModelObject,
updatePayload: ModelObject,
wrapInTransaction?: boolean,
): Promise<ModelContract>
}
6 changes: 6 additions & 0 deletions src/Orm/Relations/BelongsTo/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ export class BelongsToQueryBuilder
return related
}

public async updateOrCreate (
_search: ModelObject,
_updatePayload: ModelObject,
_wrapInTransaction?: boolean,
): Promise<any> {}

/**
* Create many not allowed for belongsTo
*/
Expand Down
41 changes: 41 additions & 0 deletions src/Orm/Relations/HasMany/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,45 @@ export class HasManyQueryBuilder
await this.saveMany(relatedModels, wrapInTransaction)
return relatedModels
}

/**
* Update the related model instance or create a new one
*/
public async updateOrCreate (
search: ModelObject,
updatePayload: ModelObject,
wrapInTransaction: boolean = true,
): Promise<any> {
if (Array.isArray(this._parent)) {
throw new Error('Cannot call "updateOrCreate" with multiple parents')
}

/**
* Callback is invoked after the parent is persisted, so that we can
* read the foreign key value
*/
const callback = (parent: ModelContract) => {
const foreignKey = this._relation.foreignKey
const foreignKeyValue = this.$getRelatedValue(parent, this._relation.localKey, 'updateOrCreate')

return {
searchPayload: Object.assign({ [foreignKey]: foreignKeyValue }, search),
updatePayload,
}
}

/**
* Wrap in transaction when parent has not been persisted
* to ensure consistency
*/
let trx: TransactionClientContract | undefined
if (!this._parent.$persisted && wrapInTransaction) {
trx = await this.client.transaction()
}

if (trx) {
return this.$updateOrCreateInTrx(this._parent, callback, trx)
}
return this.$updateOrCreate(this._parent, callback)
}
}
4 changes: 4 additions & 0 deletions src/Orm/Relations/HasManyThrough/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,8 @@ export class HasManyThroughQueryBuilder
public async createMany (): Promise<any> {
return this.save()
}

public async updateOrCreate (): Promise<any> {
return this.save()
}
}
99 changes: 79 additions & 20 deletions src/Orm/Relations/HasOne/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
import knex from 'knex'
import { Exception } from '@poppinss/utils'
import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database'
import { HasOneQueryBuilderContract, ModelContract, ModelObject } from '@ioc:Adonis/Lucid/Model'
import {
ModelObject,
ModelContract,
ModelQueryBuilderContract,
HasOneQueryBuilderContract,
} from '@ioc:Adonis/Lucid/Model'

import { HasOne } from './index'
import { unique } from '../../../utils'
Expand All @@ -36,40 +41,49 @@ export class HasOneQueryBuilder extends BaseRelationQueryBuilder implements HasO
}

/**
* Applies constraints for `select`, `update` and `delete` queries. The
* inserts are not allowed directly and one must use `save` method
* instead.
* Apply relationship constraints on a given query builder instance
*/
public applyConstraints () {
/**
* Avoid adding it for multiple times
*/
if (this.$appliedConstraints) {
return this
}

this.$appliedConstraints = true

private applyContraintsTo (
builder: ModelQueryBuilderContract<any, any>,
queryAction: string,
) {
/**
* Constraint for multiple parents
*/
if (Array.isArray(this._parent)) {
const values = unique(this._parent.map((parentInstance) => {
return this.$getRelatedValue(parentInstance, this._relation.localKey)
return this.$getRelatedValue(parentInstance, this._relation.localKey, queryAction)
}))
return this.whereIn(this._relation.foreignAdapterKey, values)
builder.whereIn(this._relation.foreignAdapterKey, values)
return
}

/**
* Constraint for one parent
*/
const value = this.$getRelatedValue(this._parent, this._relation.localKey)
this.where(this._relation.foreignAdapterKey, value)
const value = this.$getRelatedValue(this._parent, this._relation.localKey, queryAction)
builder.where(this._relation.foreignAdapterKey, value)

if (!['update', 'delete'].includes(this.$queryAction())) {
this.limit(1)
if (!['update', 'delete'].includes(queryAction)) {
builder.limit(1)
}
}

/**
* Applies constraints for `select`, `update` and `delete` queries. The
* inserts are not allowed directly and one must use `save` method
* instead.
*/
public applyConstraints () {
/**
* Avoid adding it for multiple times
*/
if (this.$appliedConstraints) {
return this
}

this.$appliedConstraints = true
this.applyContraintsTo(this, this.$queryAction())
return this
}

Expand Down Expand Up @@ -114,6 +128,47 @@ export class HasOneQueryBuilder extends BaseRelationQueryBuilder implements HasO
return related
}

/**
* Update the related model instance or create a new one
*/
public async updateOrCreate (
search: ModelObject,
updatePayload: ModelObject,
wrapInTransaction: boolean = true,
): Promise<any> {
if (Array.isArray(this._parent)) {
throw new Error('Cannot call "updateOrCreate" with multiple parents')
}

/**
* Callback is invoked after the parent is persisted, so that we can
* read the foreign key value
*/
const callback = (parent: ModelContract) => {
const foreignKey = this._relation.foreignKey
const foreignKeyValue = this.$getRelatedValue(parent, this._relation.localKey, 'updateOrCreate')

return {
searchPayload: Object.assign({ [foreignKey]: foreignKeyValue }, search),
updatePayload,
}
}

/**
* Wrap in transaction when parent has not been persisted
* to ensure consistency
*/
let trx: TransactionClientContract | undefined
if (!this._parent.$persisted && wrapInTransaction) {
trx = await this.client.transaction()
}

if (trx) {
return this.$updateOrCreateInTrx(this._parent, callback, trx)
}
return this.$updateOrCreate(this._parent, callback)
}

/**
* Save many is not allowed by HasOne
*/
Expand All @@ -127,4 +182,8 @@ export class HasOneQueryBuilder extends BaseRelationQueryBuilder implements HasO
public createMany (): Promise<any> {
throw new Exception(`Cannot create many of ${this._relation.model.name}.${this._relation.relationName}. Use create instead.`)
}

public updateOrCreateMany (): Promise<any> {
return this.createMany()
}
}
Loading

0 comments on commit 61e7d7c

Please sign in to comment.