diff --git a/adonis-typings/model.ts b/adonis-typings/model.ts index eb6fbeb6..63bc5162 100644 --- a/adonis-typings/model.ts +++ b/adonis-typings/model.ts @@ -172,7 +172,7 @@ declare module '@ioc:Adonis/Lucid/Model' { * Lookup map required for related method */ type RelationsQueryBuildersMap = { - 'unknown': ModelExecuteableQueryBuilder, + 'unknown': BaseRelationQueryBuilderContract & ExcutableQueryBuilderContract, 'hasOne': HasOneQueryBuilderContract & ExcutableQueryBuilderContract, 'hasMany': HasManyQueryBuilderContract & ExcutableQueryBuilderContract, 'belongsTo': BelongsToQueryBuilderContract & ExcutableQueryBuilderContract, @@ -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 @@ -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 = BelongsToQueryBuilderContract | - HasOneQueryBuilderContract | - HasManyQueryBuilderContract | - ManyToManyQueryBuilderContract | - HasManyThroughQueryBuilderContract + export interface BaseRelationQueryBuilderContract extends ModelQueryBuilderContract { + applyConstraints (): this - /** - * Shae of has belongs to query builder contract - */ - export interface BelongsToQueryBuilderContract extends ModelQueryBuilderContract { /** * Execute and get first result */ @@ -245,36 +241,52 @@ declare module '@ioc:Adonis/Lucid/Model' { * Return the first matching row or fail */ firstOrFail (): Promise - } - /** - * Shae of has one relationship query builder - */ - export interface HasOneQueryBuilderContract extends ModelQueryBuilderContract { /** - * Execute and get first result + * Save the related model. */ - first (): Promise + save (model: T, wrapInTransaction?: boolean): Promise /** - * Return the first matching row or fail + * Save the related model. */ - firstOrFail (): Promise + saveMany (model: T[], wrapInTransaction?: boolean): Promise } /** - * Shae of has many relationship query builder + * A union of relation relations query builders */ - export interface HasManyQueryBuilderContract extends ModelQueryBuilderContract { + type RelationQueryBuilderContract = BelongsToQueryBuilderContract | + HasOneQueryBuilderContract | + HasManyQueryBuilderContract | + ManyToManyQueryBuilderContract | + HasManyThroughQueryBuilderContract + + /** + * Shae of has belongs to query builder contract + */ + export interface BelongsToQueryBuilderContract extends BaseRelationQueryBuilderContract { /** - * Execute and get first result + * Associate related model. */ - first (): Promise + associate (model: T, wrapInTransaction?: boolean): Promise /** - * Return the first matching row or fail + * Dissociate all relationships. */ - firstOrFail (): Promise + dissociate (): Promise + } + + /** + * Shae of has one relationship query builder + */ + export interface HasOneQueryBuilderContract extends BaseRelationQueryBuilderContract { + } + + /** + * Shae of has many relationship query builder + */ + export interface HasManyQueryBuilderContract extends BaseRelationQueryBuilderContract { } /** @@ -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 extends ModelQueryBuilderContract { + export interface ManyToManyQueryBuilderContract extends BaseRelationQueryBuilderContract { pivotColumns (columns: string[]): this wherePivot: WherePivot @@ -319,29 +331,38 @@ declare module '@ioc:Adonis/Lucid/Model' { andWhereNotInPivot: WhereInPivot /** - * Execute and get first result + * Save related model */ - first (): Promise + save (model: T, wrapInTransaction?: boolean, checkExisting?: boolean): Promise /** - * Return the first matching row or fail + * Save related many */ - firstOrFail (): Promise + saveMany (model: T[], wrapInTransaction?: boolean, checkExisting?: boolean): Promise + + /** + * Attach related + */ + attach ( + ids: (string | number)[] | { [key: string]: any }, + checkExisting?: boolean, + ): Promise + + // /** + // * Attach related + // */ + // detach (ids: any[]): Promise + + // /** + // * Attach related + // */ + // sync (ids: any[], detach: boolean): Promise } /** * Shae of has many through relationship query builder */ - export interface HasManyThroughQueryBuilderContract extends ModelQueryBuilderContract { - /** - * Execute and get first result - */ - first (): Promise - - /** - * Return the first matching row or fail - */ - firstOrFail (): Promise + export interface HasManyThroughQueryBuilderContract extends BaseRelationQueryBuilderContract { } /** diff --git a/example/index.ts b/example/index.ts index 3e7df2a2..09a43d56 100644 --- a/example/index.ts +++ b/example/index.ts @@ -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() diff --git a/src/Database/QueryBuilder/Chainable.ts b/src/Database/QueryBuilder/Chainable.ts index 6b2c7c2e..b54cd1b3 100644 --- a/src/Database/QueryBuilder/Chainable.ts +++ b/src/Database/QueryBuilder/Chainable.ts @@ -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 } diff --git a/src/Orm/Relations/Base/QueryBuilder.ts b/src/Orm/Relations/Base/QueryBuilder.ts new file mode 100644 index 00000000..363e987c --- /dev/null +++ b/src/Orm/Relations/Base/QueryBuilder.ts @@ -0,0 +1,121 @@ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +/// + +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 +{ + 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 ( + 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 ( + 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 + public abstract async saveMany (model: ModelContract[], wrapInTransaction?: boolean): Promise +} diff --git a/src/Orm/Relations/BelongsTo/QueryBuilder.ts b/src/Orm/Relations/BelongsTo/QueryBuilder.ts index b6ad6cdc..efed422d 100644 --- a/src/Orm/Relations/BelongsTo/QueryBuilder.ts +++ b/src/Orm/Relations/BelongsTo/QueryBuilder.ts @@ -10,24 +10,135 @@ /// import knex from 'knex' -import { BelongsToQueryBuilderContract, RelationContract } from '@ioc:Adonis/Lucid/Model' -import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' +import { uniq } from 'lodash' +import { Exception } from '@poppinss/utils' -import { ModelQueryBuilder } from '../../QueryBuilder' +import { ModelContract, BelongsToQueryBuilderContract } from '@ioc:Adonis/Lucid/Model' +import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database' + +import { BelongsTo } from './index' +import { BaseRelationQueryBuilder } from '../Base/QueryBuilder' /** * Exposes the API for interacting with belongs relationship */ -export class BelongsToQueryBuilder extends ModelQueryBuilder implements BelongsToQueryBuilderContract { +export class BelongsToQueryBuilder + extends BaseRelationQueryBuilder + implements BelongsToQueryBuilderContract +{ constructor ( builder: knex.QueryBuilder, - private _relation: RelationContract, + private _relation: BelongsTo, client: QueryClientContract, + private _parent: ModelContract | ModelContract[], ) { - super(builder, _relation.relatedModel(), client, (userFn) => { + super(builder, _relation, client, (userFn) => { return (builder) => { - userFn(new BelongsToQueryBuilder(builder, this._relation, this.client)) + userFn(new BelongsToQueryBuilder(builder, this._relation, this.client, this._parent)) } }) } + + /** + * 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 + + /** + * Constraint for multiple parents + */ + if (Array.isArray(this._parent)) { + const values = uniq(this._parent.map((parentInstance) => { + return this.$getRelatedValue(parentInstance, this._relation.foreignKey) + })) + return this.whereIn(this._relation.localAdapterKey, values) + } + + /** + * Constraint for one parent + */ + const value = this.$getRelatedValue(this._parent, this._relation.foreignKey) + return this.where(this._relation.localAdapterKey, value).limit(1) + } + + /** + * Persists related model instance by setting the FK + */ + protected async $persist ( + parent: T, + related: V | V[], + cb: (parent: T, related: V) => void, + ) { + related = related as V + + /** + * Copying options and trx to make sure relation is using + * the same options from the parent model + */ + related.$trx = parent.$trx + related.$options = parent.$options + await related.save() + + cb(parent, related) + return parent.save() + } + + /** + * Save related + */ + public async save (related: ModelContract, wrapInTransaction: boolean = true) { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + /** + * Wrap in transaction when parent has not been persisted + * to ensure consistency + */ + let trx: TransactionClientContract | undefined + if (!related.$persisted && wrapInTransaction) { + trx = await this.client.transaction() + } + + const callback = (parent, related) => { + parent[this._relation.foreignKey] = this.$getRelatedValue(related, this._relation.localKey) + } + + if (trx) { + return this.$persistInTrx(this._parent, related, trx, callback) + } else { + return this.$persist(this._parent, related, callback) + } + } + + /** + * Alias for save, since `associate` feels more natural + */ + public async associate (related: ModelContract, wrapInTransaction: boolean = true) { + return this.save(related, wrapInTransaction) + } + + /** + * Remove relation + */ + public async dissociate () { + } + + /** + * Save many not allowed for belongsTo + */ + public async saveMany () { + throw new Exception(`Cannot save many of ${this._relation.model.name}.${this._relation.relationName}. Use associate instead.`) + } } diff --git a/src/Orm/Relations/BelongsTo/index.ts b/src/Orm/Relations/BelongsTo/index.ts index 9185c7a3..0c490353 100644 --- a/src/Orm/Relations/BelongsTo/index.ts +++ b/src/Orm/Relations/BelongsTo/index.ts @@ -10,7 +10,7 @@ /// import { Exception } from '@poppinss/utils' -import { camelCase, snakeCase, uniq } from 'lodash' +import { camelCase, snakeCase } from 'lodash' import { ModelContract, @@ -60,7 +60,7 @@ export class BelongsTo implements RelationContract { /** * Key to be used for serializing the relationship */ - public serializeAs = this._options.serializeAs || snakeCase(this._relationName) + public serializeAs = this._options.serializeAs || snakeCase(this.relationName) /** * A flag to know if model keys valid for executing database queries or not @@ -68,9 +68,9 @@ export class BelongsTo implements RelationContract { public booted: boolean = false constructor ( - private _relationName: string, + public relationName: string, private _options: BaseRelationNode, - private _model: ModelConstructorContract, + public model: ModelConstructorContract, ) { this._ensureRelatedModel() } @@ -94,10 +94,10 @@ export class BelongsTo implements RelationContract { * the keys validation, since they may be added after defining the relationship. */ private _validateKeys () { - const relationRef = `${this._model.name}.${this._relationName}` + const relationRef = `${this.model.name}.${this.relationName}` - if (!this._model.$hasColumn(this.foreignKey)) { - const ref = `${this._model.name}.${this.foreignKey}` + if (!this.model.$hasColumn(this.foreignKey)) { + const ref = `${this.model.name}.${this.foreignKey}` throw new Exception( `${ref} required by ${relationRef} relation is missing`, 500, @@ -115,28 +115,6 @@ export class BelongsTo implements RelationContract { } } - /** - * Raises exception when value for the foreign key is missing on the model instance. This will - * make the query fail - */ - private _ensureValue (value: any, action: string = 'preload') { - if (value === undefined) { - throw new Exception( - `Cannot ${action} ${this._relationName}, value of ${this._model.name}.${this.foreignKey} is undefined`, - 500, - ) - } - - return value - } - - /** - * Returns the belongs to query builder - */ - private _getQueryBuilder (client: QueryClientContract) { - return new BelongsToQueryBuilder(client.knexQuery(), this, client) - } - /** * Compute keys */ @@ -159,31 +137,23 @@ export class BelongsTo implements RelationContract { * Keys for the adapter */ this.localAdapterKey = this.relatedModel().$getColumn(this.localKey)!.castAs - this.foreignAdapterKey = this._model.$getColumn(this.foreignKey)!.castAs + this.foreignAdapterKey = this.model.$getColumn(this.foreignKey)!.castAs this.booted = true } /** * Returns eager query for a single parent model instance */ - public getQuery (parent: ModelContract, client: QueryClientContract) { - const value = parent[this.foreignKey] - - return this._getQueryBuilder(client) - .where(this.localAdapterKey, this._ensureValue(value)) - .limit(1) + public getQuery (parent: ModelContract, client: QueryClientContract): any { + return new BelongsToQueryBuilder(client.knexQuery(), this, client, parent) } /** * 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._getQueryBuilder(client).whereIn(this.localAdapterKey, values) + public getEagerQuery (parents: ModelContract[], client: QueryClientContract): any { + return new BelongsToQueryBuilder(client.knexQuery(), this, client, parents) } /** @@ -194,7 +164,7 @@ export class BelongsTo implements RelationContract { return } - model.$setRelated(this._relationName as keyof typeof model, related) + model.$setRelated(this.relationName as keyof typeof model, related) } /** diff --git a/src/Orm/Relations/HasMany/QueryBuilder.ts b/src/Orm/Relations/HasMany/QueryBuilder.ts index 338344a6..12e4b8f1 100644 --- a/src/Orm/Relations/HasMany/QueryBuilder.ts +++ b/src/Orm/Relations/HasMany/QueryBuilder.ts @@ -10,24 +10,122 @@ /// import knex from 'knex' -import { HasManyQueryBuilderContract, RelationContract } from '@ioc:Adonis/Lucid/Model' -import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' +import { uniq } from 'lodash' +import { HasManyQueryBuilderContract, ModelContract } from '@ioc:Adonis/Lucid/Model' +import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database' -import { ModelQueryBuilder } from '../../QueryBuilder' +import { HasMany } from './index' +import { BaseRelationQueryBuilder } from '../Base/QueryBuilder' /** * Exposes the API for interacting with has many relationship */ -export class HasManyQueryBuilder extends ModelQueryBuilder implements HasManyQueryBuilderContract { +export class HasManyQueryBuilder + extends BaseRelationQueryBuilder + implements HasManyQueryBuilderContract +{ constructor ( builder: knex.QueryBuilder, - private _relation: RelationContract, + private _relation: HasMany, client: QueryClientContract, + private _parent: ModelContract | ModelContract[], ) { - super(builder, _relation.relatedModel(), client, (userFn) => { + super(builder, _relation, client, (userFn) => { return (builder) => { - userFn(new HasManyQueryBuilder(builder, this._relation, this.client)) + userFn(new HasManyQueryBuilder(builder, this._relation, this.client, this._parent)) } }) } + + /** + * 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 + + /** + * Constraint for multiple parents + */ + if (Array.isArray(this._parent)) { + const values = uniq(this._parent.map((parentInstance) => { + return this.$getRelatedValue(parentInstance, this._relation.localKey) + })) + return this.whereIn(this._relation.foreignAdapterKey, values) + } + + /** + * Constraint for one parent + */ + const value = this.$getRelatedValue(this._parent, this._relation.localKey) + return this.where(this._relation.foreignAdapterKey, value) + } + + /** + * Save related instance. Internally a transaction will be created + * when parent model is not persisted. Set `wrapInTransaction=false` + * as 2nd argument to turn it off + */ + public async save (related: ModelContract, wrapInTransaction: boolean = true): Promise { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + /** + * 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() + } + + const callback = (parent, related) => { + related[this._relation.foreignKey] = this.$getRelatedValue(parent, this._relation.localKey) + } + + if (trx) { + return this.$persistInTrx(this._parent, related, trx, callback) + } else { + return this.$persist(this._parent, related, callback) + } + } + + /** + * Save many of the related models + */ + public async saveMany (related: ModelContract[], wrapInTransaction: boolean = true): Promise { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + /** + * Wrap in transaction when wrapInTransaction is not set to false. So that + * we rollback to initial state, when one or more fails + */ + let trx: TransactionClientContract | undefined + if (wrapInTransaction) { + trx = await this.client.transaction() + } + + const callback = (parent, related) => { + related[this._relation.foreignKey] = this.$getRelatedValue(parent, this._relation.localKey) + } + + if (trx) { + return this.$persistInTrx(this._parent, related, trx!, callback) + } else { + return this.$persist(this._parent, related, callback) + } + } } diff --git a/src/Orm/Relations/HasMany/index.ts b/src/Orm/Relations/HasMany/index.ts index 3b65116c..ca0a63e5 100644 --- a/src/Orm/Relations/HasMany/index.ts +++ b/src/Orm/Relations/HasMany/index.ts @@ -32,16 +32,15 @@ export class HasMany extends HasOneOrMany { /** * Returns the query builder for has many relationship */ - protected $getQueryBuilder (client: QueryClientContract): any { - return new HasManyQueryBuilder(client.knexQuery(), this, client) + protected $getQueryBuilder (client: QueryClientContract, parent: ModelContract | ModelContract[]): any { + return new HasManyQueryBuilder(client.knexQuery(), this, client, parent) } /** * Returns query for the relationship with applied constraints */ public getQuery (parent: ModelContract, client: QueryClientContract): any { - const value = parent[this.localKey] - return this.$getQueryBuilder(client).where(this.foreignAdapterKey, this.$ensureValue(value)) + return this.$getQueryBuilder(client, parent) } /** diff --git a/src/Orm/Relations/HasManyThrough/QueryBuilder.ts b/src/Orm/Relations/HasManyThrough/QueryBuilder.ts index b96a8f3a..e809644e 100644 --- a/src/Orm/Relations/HasManyThrough/QueryBuilder.ts +++ b/src/Orm/Relations/HasManyThrough/QueryBuilder.ts @@ -10,24 +10,90 @@ /// import knex from 'knex' -import { HasManyThroughQueryBuilderContract, RelationContract } from '@ioc:Adonis/Lucid/Model' +import { uniq } from 'lodash' +import { Exception } from '@poppinss/utils' +import { HasManyThroughQueryBuilderContract, ModelContract } from '@ioc:Adonis/Lucid/Model' import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' -import { ModelQueryBuilder } from '../../QueryBuilder' +import { HasManyThrough } from './index' +import { BaseRelationQueryBuilder } from '../Base/QueryBuilder' /** * Exposes the API for interacting with has many relationship */ -export class HasManyThroughQueryBuilder extends ModelQueryBuilder implements HasManyThroughQueryBuilderContract { +export class HasManyThroughQueryBuilder + extends BaseRelationQueryBuilder + implements HasManyThroughQueryBuilderContract +{ constructor ( builder: knex.QueryBuilder, - private _relation: RelationContract, + private _relation: HasManyThrough, client: QueryClientContract, + private _parent: ModelContract | ModelContract[], ) { - super(builder, _relation.relatedModel(), client, (userFn) => { + super(builder, _relation, client, (userFn) => { return (builder) => { - userFn(new HasManyThroughQueryBuilder(builder, this._relation, this.client)) + userFn(new HasManyThroughQueryBuilder(builder, this._relation, this.client, this._parent)) } }) } + + /** + * 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 + + const throughTable = this._relation.throughModel().$table + const relatedTable = this._relation.relatedModel().$table + + /** + * Select * from related model and through foreign adapter key + */ + this.select( + `${relatedTable}.*`, + `${throughTable}.${this._relation.foreignAdapterKey} as through_${this._relation.foreignAdapterKey}`, + ) + + /** + * Add inner join + */ + this.innerJoin( + `${throughTable}`, + `${throughTable}.${this._relation.throughLocalAdapterKey}`, + `${relatedTable}.${this._relation.throughForeignAdapterKey}`, + ) + + /** + * Constraint for multiple parents + */ + if (Array.isArray(this._parent)) { + const values = uniq(this._parent.map((parentInstance) => { + return this.$getRelatedValue(parentInstance, this._relation.localKey) + })) + return this.whereIn(`${throughTable}.${this._relation.foreignAdapterKey}`, values) + } + + /** + * Constraint for one parent + */ + const value = this.$getRelatedValue(this._parent, this._relation.localKey) + return this.where(`${throughTable}.${this._relation.foreignAdapterKey}`, value) + } + + public async save () { + throw new Exception(`Has many through doesn\'t support saving relations`) + } + + public async saveMany () { + throw new Exception(`Has many through doesn\'t support saving relations`) + } } diff --git a/src/Orm/Relations/HasManyThrough/index.ts b/src/Orm/Relations/HasManyThrough/index.ts index 4bea62de..d6653947 100644 --- a/src/Orm/Relations/HasManyThrough/index.ts +++ b/src/Orm/Relations/HasManyThrough/index.ts @@ -10,14 +10,13 @@ /// import { Exception } from '@poppinss/utils' -import { camelCase, snakeCase, uniq } from 'lodash' +import { camelCase, snakeCase } from 'lodash' import { ModelContract, RelationContract, ThroughRelationNode, ModelConstructorContract, - ModelQueryBuilderContract, } from '@ioc:Adonis/Lucid/Model' import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' @@ -85,7 +84,7 @@ export class HasManyThrough implements RelationContract { /** * Key to be used for serializing the relationship */ - public serializeAs = this._options.serializeAs || snakeCase(this._relationName) + public serializeAs = this._options.serializeAs || snakeCase(this.relationName) /** * A flag to know if model keys valid for executing database queries or not @@ -93,9 +92,9 @@ export class HasManyThrough implements RelationContract { public booted: boolean = false constructor ( - private _relationName: string, + public relationName: string, private _options: ThroughRelationNode, - private _model: ModelConstructorContract, + public model: ModelConstructorContract, ) { this._ensureRelatedModel() } @@ -119,10 +118,10 @@ export class HasManyThrough implements RelationContract { * the keys validation, since they may be added after defining the relationship. */ private _validateKeys () { - const relationRef = `${this._model.name}.${this._relationName}` + const relationRef = `${this.model.name}.${this.relationName}` - if (!this._model.$hasColumn(this.localKey)) { - const ref = `${this._model.name}.${this.localKey}` + if (!this.model.$hasColumn(this.localKey)) { + const ref = `${this.model.name}.${this.localKey}` throw new Exception( `${ref} required by ${relationRef} relation is missing`, 500, @@ -158,52 +157,6 @@ export class HasManyThrough implements RelationContract { } } - /** - * 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 - } - - /** - * Adds the select columns - */ - private _addSelect (query: ModelQueryBuilderContract) { - query.select( - `${this.relatedModel().$table}.*`, - `${this.throughModel().$table}.${this.foreignAdapterKey} as through_${this.foreignAdapterKey}`, - ) - } - - /** - * Adds the join clause for the select query - */ - private _addJoin (query: ModelQueryBuilderContract) { - const throughTable = this.throughModel().$table - const relatedTable = this.relatedModel().$table - - query.innerJoin( - `${throughTable}`, - `${throughTable}.${this.throughLocalAdapterKey}`, - `${relatedTable}.${this.throughForeignAdapterKey}`, - ) - } - - /** - * Returns the belongs to query builder - */ - private _getQueryBuilder (client: QueryClientContract) { - return new HasManyThroughQueryBuilder(client.knexQuery(), this, client) - } - /** * Compute keys */ @@ -212,12 +165,12 @@ export class HasManyThrough implements RelationContract { return } - this.localKey = this._options.localKey || this._model.$primaryKey - this.foreignKey = this._options.foreignKey || camelCase(`${this._model.name}_${this._model.$primaryKey}`) + 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.throughLocalKey = this._options.localKey || this.throughModel().$primaryKey this.throughForeignKey = this._options.throughForeignKey - || camelCase(`${this.throughModel().name}_${this.throughModel().$primaryKey}`) // user_id (user) + || camelCase(`${this.throughModel().name}_${this.throughModel().$primaryKey}`) /** * Validate computed keys to ensure they are valid @@ -227,7 +180,7 @@ export class HasManyThrough implements RelationContract { /** * Keys for the adapter */ - this.localAdapterKey = this._model.$getColumn(this.localKey)!.castAs + 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 @@ -239,31 +192,14 @@ export class HasManyThrough implements RelationContract { * eagerloading */ public getEagerQuery (parents: ModelContract[], client: QueryClientContract): any { - const values = uniq(parents.map((parentInstance) => { - return this._ensureValue(parentInstance[this.localKey]) - })) - - const throughTable = this.throughModel().$table - const query = this._getQueryBuilder(client) - - this._addJoin(query) - this._addSelect(query) - - return query.whereIn(`${throughTable}.${this.foreignAdapterKey}`, values) + return new HasManyThroughQueryBuilder(client.knexQuery(), this, client, parents) } /** * Returns query for the relationship with applied constraints */ public getQuery (parent: ModelContract, client: QueryClientContract): any { - const value = parent[this.localKey] - const throughTable = this.throughModel().$table - const query = this._getQueryBuilder(client) - - this._addJoin(query) - this._addSelect(query) - - return query.where(`${throughTable}.${this.foreignAdapterKey}`, this._ensureValue(value)) + return new HasManyThroughQueryBuilder(client.knexQuery(), this, client, parent) } /** @@ -274,7 +210,7 @@ export class HasManyThrough implements RelationContract { return } - parent.$setRelated(this._relationName as keyof typeof parent, related) + parent.$setRelated(this.relationName as keyof typeof parent, related) } /** diff --git a/src/Orm/Relations/HasOne/QueryBuilder.ts b/src/Orm/Relations/HasOne/QueryBuilder.ts index 3da3c91b..d966cb8d 100644 --- a/src/Orm/Relations/HasOne/QueryBuilder.ts +++ b/src/Orm/Relations/HasOne/QueryBuilder.ts @@ -10,24 +10,98 @@ /// import knex from 'knex' -import { HasOneQueryBuilderContract, RelationContract } from '@ioc:Adonis/Lucid/Model' -import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' +import { uniq } from 'lodash' +import { Exception } from '@poppinss/utils' +import { HasOneQueryBuilderContract, ModelContract } from '@ioc:Adonis/Lucid/Model' +import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database' -import { ModelQueryBuilder } from '../../QueryBuilder' +import { HasOne } from './index' +import { BaseRelationQueryBuilder } from '../Base/QueryBuilder' /** * Exposes the API for interacting with has many relationship */ -export class HasOneQueryBuilder extends ModelQueryBuilder implements HasOneQueryBuilderContract { +export class HasOneQueryBuilder extends BaseRelationQueryBuilder implements HasOneQueryBuilderContract { constructor ( builder: knex.QueryBuilder, - private _relation: RelationContract, + private _relation: HasOne, client: QueryClientContract, + private _parent: ModelContract | ModelContract[], ) { - super(builder, _relation.relatedModel(), client, (userFn) => { + super(builder, _relation, client, (userFn) => { return (builder) => { - userFn(new HasOneQueryBuilder(builder, this._relation, this.client)) + userFn(new HasOneQueryBuilder(builder, this._relation, this.client, _parent)) } }) } + + /** + * 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 + + /** + * Constraint for multiple parents + */ + if (Array.isArray(this._parent)) { + const values = uniq(this._parent.map((parentInstance) => { + return this.$getRelatedValue(parentInstance, this._relation.localKey) + })) + return this.whereIn(this._relation.foreignAdapterKey, values) + } + + /** + * Constraint for one parent + */ + const value = this.$getRelatedValue(this._parent, this._relation.localKey) + return this.where(this._relation.foreignAdapterKey, value).limit(1) + } + + /** + * Save related instance. Internally a transaction will be created + * when parent model is not persisted. Set `wrapInTransaction=false` + * as 2nd argument to turn it off + */ + public async save (related: ModelContract, wrapInTransaction: boolean = true): Promise { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + /** + * 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() + } + + const callback = (parent, related) => { + related[this._relation.foreignKey] = this.$getRelatedValue(parent, this._relation.localKey) + } + + if (trx) { + return this.$persistInTrx(this._parent, related, trx, callback) + } else { + return this.$persist(this._parent, related, callback) + } + } + + /** + * Save many is not allowed by HasOne + */ + public async saveMany () { + throw new Exception(`Cannot save many of ${this._relation.model.name}.${this._relation.relationName}. Use save instead.`) + } } diff --git a/src/Orm/Relations/HasOne/index.ts b/src/Orm/Relations/HasOne/index.ts index 5b9521a7..7251eef4 100644 --- a/src/Orm/Relations/HasOne/index.ts +++ b/src/Orm/Relations/HasOne/index.ts @@ -32,19 +32,15 @@ export class HasOne extends HasOneOrMany { /** * Returns the query builder for has many relationship */ - protected $getQueryBuilder (client: QueryClientContract) { - return new HasOneQueryBuilder(client.knexQuery(), this, client) + protected $getQueryBuilder (client: QueryClientContract, parent: ModelContract | ModelContract[]) { + return new HasOneQueryBuilder(client.knexQuery(), this, client, parent) } /** * Returns query for the relationship with applied constraints */ public getQuery (parent: ModelContract, client: QueryClientContract): any { - const value = parent[this.localKey] - - return this.$getQueryBuilder(client) - .where(this.foreignAdapterKey, this.$ensureValue(value)) - .limit(1) + return this.$getQueryBuilder(client, parent) } /** diff --git a/src/Orm/Relations/HasOneOrMany.ts b/src/Orm/Relations/HasOneOrMany.ts index 32a97ba3..d7c87e17 100644 --- a/src/Orm/Relations/HasOneOrMany.ts +++ b/src/Orm/Relations/HasOneOrMany.ts @@ -10,7 +10,7 @@ /// import { Exception } from '@poppinss/utils' -import { camelCase, snakeCase, uniq } from 'lodash' +import { camelCase, snakeCase } from 'lodash' import { ModelContract, @@ -55,7 +55,7 @@ export abstract class HasOneOrMany implements RelationContract { /** * Key to be used for serializing the relationship */ - public serializeAs = this._options.serializeAs || snakeCase(this._relationName) + public serializeAs = this._options.serializeAs || snakeCase(this.relationName) /** * A flag to know if model keys valid for executing database queries or not @@ -63,9 +63,9 @@ export abstract class HasOneOrMany implements RelationContract { public booted: boolean = false constructor ( - private _relationName: string, + public relationName: string, private _options: BaseRelationNode, - private _model: ModelConstructorContract, + public model: ModelConstructorContract, ) { this._ensureRelatedModel() } @@ -89,10 +89,10 @@ export abstract class HasOneOrMany implements RelationContract { * the keys validation, since they may be added after defining the relationship. */ private _validateKeys () { - const relationRef = `${this._model.name}.${this._relationName}` + const relationRef = `${this.model.name}.${this.relationName}` - if (!this._model.$hasColumn(this.localKey)) { - const ref = `${this._model.name}.${this.localKey}` + if (!this.model.$hasColumn(this.localKey)) { + const ref = `${this.model.name}.${this.localKey}` throw new Exception( `${ref} required by ${relationRef} relation is missing`, 500, @@ -110,21 +110,6 @@ export abstract class HasOneOrMany implements RelationContract { } } - /** - * Raises exception when value for the local key is missing on the model instance. This will - * make the query fail - */ - protected $ensureValue (value: any, action: string = 'preload') { - if (value === undefined) { - throw new Exception( - `Cannot ${action} ${this._relationName}, value of ${this._model.name}.${this.localKey} is undefined`, - 500, - ) - } - - return value - } - /** * Must be implemented by main class */ @@ -138,7 +123,10 @@ export abstract class HasOneOrMany implements RelationContract { /** * Must be implemented by parent class */ - protected abstract $getQueryBuilder (client: QueryClientContract): any + protected abstract $getQueryBuilder ( + client: QueryClientContract, + parent: ModelContract | ModelContract[], + ): any /** * Compute keys @@ -148,8 +136,8 @@ export abstract class HasOneOrMany implements RelationContract { return } - this.localKey = this._options.localKey || this._model.$primaryKey - this.foreignKey = this._options.foreignKey || camelCase(`${this._model.name}_${this._model.$primaryKey}`) + this.localKey = this._options.localKey || this.model.$primaryKey + this.foreignKey = this._options.foreignKey || camelCase(`${this.model.name}_${this.model.$primaryKey}`) /** * Validate computed keys to ensure they are valid @@ -159,7 +147,7 @@ export abstract class HasOneOrMany implements RelationContract { /** * Keys for the adapter */ - this.localAdapterKey = this._model.$getColumn(this.localKey)!.castAs + this.localAdapterKey = this.model.$getColumn(this.localKey)!.castAs this.foreignAdapterKey = this.relatedModel().$getColumn(this.foreignKey)!.castAs this.booted = true } @@ -169,12 +157,7 @@ export abstract class HasOneOrMany implements RelationContract { * eagerloading */ public getEagerQuery (parents: ModelContract[], client: QueryClientContract) { - const values = uniq(parents.map((parentInstance) => { - return this.$ensureValue(parentInstance[this.localKey]) - })) - - return this.$getQueryBuilder(client) - .whereIn(this.foreignAdapterKey, values) + return this.$getQueryBuilder(client, parents) } /** @@ -185,6 +168,6 @@ export abstract class HasOneOrMany implements RelationContract { return } - parent.$setRelated(this._relationName as keyof typeof parent, related) + parent.$setRelated(this.relationName as keyof typeof parent, related) } } diff --git a/src/Orm/Relations/ManyToMany/QueryBuilder.ts b/src/Orm/Relations/ManyToMany/QueryBuilder.ts index 294ea30c..ecad0349 100644 --- a/src/Orm/Relations/ManyToMany/QueryBuilder.ts +++ b/src/Orm/Relations/ManyToMany/QueryBuilder.ts @@ -10,23 +10,29 @@ /// import knex from 'knex' -import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' -import { RelationContract, ManyToManyQueryBuilderContract } from '@ioc:Adonis/Lucid/Model' +import { uniq, difference } from 'lodash' +import { ModelContract, ManyToManyQueryBuilderContract } from '@ioc:Adonis/Lucid/Model' +import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database' -import { ModelQueryBuilder } from '../../QueryBuilder' +import { ManyToMany } from './index' +import { BaseRelationQueryBuilder } from '../Base/QueryBuilder' /** * Query builder with many to many relationships */ -export class ManyToManyQueryBuilder extends ModelQueryBuilder implements ManyToManyQueryBuilderContract { +export class ManyToManyQueryBuilder + extends BaseRelationQueryBuilder + implements ManyToManyQueryBuilderContract +{ constructor ( builder: knex.QueryBuilder, - private _relation: RelationContract, + private _relation: ManyToMany, client: QueryClientContract, + private _parent: ModelContract | ModelContract[], ) { - super(builder, _relation.relatedModel(), client, (userFn) => { + super(builder, _relation, client, (userFn) => { return (builder) => { - userFn(new ManyToManyQueryBuilder(builder, this._relation, this.client)) + userFn(new ManyToManyQueryBuilder(builder, this._relation, this.client, this._parent)) } }) } @@ -199,4 +205,266 @@ export class ManyToManyQueryBuilder extends ModelQueryBuilder implements ManyToM })) return this } + + /** + * 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 + + /** + * Select * from related model + */ + this.select(`${this._relation.relatedModel().$table}.*`) + + /** + * Select pivot columns + */ + this.pivotColumns( + [ + this._relation.pivotForeignKey, + this._relation.pivotRelatedForeignKey, + ].concat(this._relation.extrasPivotColumns), + ) + + /** + * Add inner join + */ + this.innerJoin( + this._relation.pivotTable, + `${this._relation.relatedModel().$table}.${this._relation.relatedAdapterKey}`, + `${this._relation.pivotTable}.${this._relation.pivotRelatedForeignKey}`, + ) + + /** + * Constraint for multiple parents + */ + if (Array.isArray(this._parent)) { + const values = uniq(this._parent.map((parentInstance) => { + return this.$getRelatedValue(parentInstance, this._relation.localKey) + })) + return this.whereInPivot(this._relation.pivotForeignKey, values) + } + + /** + * Constraint for one parent + */ + const value = this.$getRelatedValue(this._parent, this._relation.localKey) + return this.wherePivot(this._relation.pivotForeignKey, value) + } + + /** + * Perists the model, related model along with the pivot entry + */ + private async _persist ( + parent: ModelContract, + related: ModelContract | ModelContract[], + checkExisting: boolean, + ) { + related = Array.isArray(related) ? related : [related] + + /** + * Persist parent and related models (if required) + */ + await this.$persist(parent, related, () => {}) + + /** + * Pull the parent model client from the adapter, so that it used the + * same connection options for creating the pivot entry + */ + const client = this._relation.model.$adapter.modelClient(parent) + + /** + * Attach the id + */ + await this._attach( + parent, + client, + related.map((relation) => this.$getRelatedValue(relation, this._relation.relatedKey)), + checkExisting, + ) + } + + /** + * Perists the model, related model along with the pivot entry inside the + * transaction. + */ + private async _persistInTransaction ( + parent: ModelContract, + related: ModelContract | ModelContract[], + trx: TransactionClientContract, + checkExisting: boolean, + ) { + related = Array.isArray(related) ? related : [related] + + try { + /** + * Setting transaction on the parent model and this will + * be copied over related model as well inside the + * $persist call + */ + parent.$trx = trx + await this.$persist(parent, related, () => {}) + + /** + * Invoking attach on the related model id and passing the transaction + * client around, so that the pivot insert is also a part of + * the transaction + */ + await this._attach( + parent, + trx, + related.map((relation) => this.$getRelatedValue(relation, this._relation.relatedKey)), + checkExisting, + ) + + /** + * Commit the transaction + */ + await trx.commit() + } catch (error) { + await trx.rollback() + throw error + } + } + + /** + * Make relation entries to the pivot table. The id's must be a reference + * to the related model primary key, and this method doesn't perform + * any checks for same. + */ + private async _attach ( + parent: ModelContract, + client: QueryClientContract, + ids: (string | number)[] | { [key: string]: any }, + checkExisting: boolean, + ) { + let idsList = uniq(Array.isArray(ids) ? ids : Object.keys(ids)) + const hasAttributes = !Array.isArray(ids) + + /** + * Pull existing pivot rows when `checkExisting = true` and persist only + * the differnce + */ + if (checkExisting) { + const existingRows = await client + .query() + .from(this._relation.pivotTable) + .select(this._relation.pivotRelatedForeignKey) + .whereIn(this._relation.pivotRelatedForeignKey, idsList) + .where( + this._relation.pivotForeignKey, + this.$getRelatedValue(parent, this._relation.localKey, 'attach'), + ) + + const existingIds = existingRows.map((row) => row[this._relation.pivotRelatedForeignKey]) + idsList = difference(idsList, existingIds) + } + + /** + * Ignore when there is nothing to insert + */ + if (!idsList.length) { + return + } + + /** + * Perform multiple inserts in one go + */ + await client + .insertQuery() + .table(this._relation.pivotTable) + .multiInsert(idsList.map((id) => { + const payload = { + [this._relation.pivotForeignKey]: this.$getRelatedValue(parent, this._relation.localKey), + [this._relation.pivotRelatedForeignKey]: id, + } + + return hasAttributes ? Object.assign(payload, ids[id]) : payload + })) + } + + /** + * Save related model instance with entry in the pivot table + */ + public async save ( + related: ModelContract, + wrapInTransaction: boolean = true, + checkExisting: boolean = true, + ): Promise { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + /** + * Wrap in transaction when wrapInTransaction is not set to false. So that + * we rollback to initial state, when one or more fails + */ + let trx: TransactionClientContract | undefined + if (wrapInTransaction) { + trx = await this.client.transaction() + } + + if (trx) { + await this._persistInTransaction(this._parent, related, trx, checkExisting) + } else { + await this._persist(this._parent, related, checkExisting) + } + } + + /** + * Save many of related model instances with entry + * in the pivot table + */ + public async saveMany ( + related: ModelContract[], + wrapInTransaction: boolean = true, + checkExisting: boolean = true, + ) { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + /** + * Wrap in transaction when wrapInTransaction is not set to false. So that + * we rollback to initial state, when one or more fails + */ + let trx: TransactionClientContract | undefined + if (wrapInTransaction) { + trx = await this.client.transaction() + } + + if (trx) { + await this._persistInTransaction(this._parent, related, trx, checkExisting) + } else { + await this._persist(this._parent, related, checkExisting) + } + } + + /** + * Attach one of more related instances + */ + public async attach ( + ids: (string | number)[] | { [key: string]: any }, + checkExisting: boolean = true, + ) { + if (Array.isArray(this._parent)) { + throw new Error('Cannot save with multiple parents') + return + } + + const client = this._relation.model.$adapter.modelClient(this._parent) + await this._attach(this._parent, client, ids, checkExisting) + } } diff --git a/src/Orm/Relations/ManyToMany/index.ts b/src/Orm/Relations/ManyToMany/index.ts index 7dff1a05..ae20dc40 100644 --- a/src/Orm/Relations/ManyToMany/index.ts +++ b/src/Orm/Relations/ManyToMany/index.ts @@ -10,14 +10,13 @@ /// import { Exception } from '@poppinss/utils' -import { snakeCase, uniq, sortBy } from 'lodash' +import { snakeCase, sortBy } from 'lodash' import { ModelContract, RelationContract, ManyToManyRelationNode, ModelConstructorContract, - ManyToManyQueryBuilderContract, } from '@ioc:Adonis/Lucid/Model' import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' @@ -85,10 +84,15 @@ export class ManyToMany implements RelationContract { */ public pivotTable: string + /** + * Extra pivot columns to extra + */ + public extrasPivotColumns: string[] = this._options.pivotColumns || [] + /** * Key to be used for serializing the relationship */ - public serializeAs = this._options.serializeAs || snakeCase(this._relationName) + public serializeAs = this._options.serializeAs || snakeCase(this.relationName) /** * A flag to know if model keys are valid for executing database queries or not @@ -96,9 +100,9 @@ export class ManyToMany implements RelationContract { public booted: boolean = false constructor ( - private _relationName: string, + public relationName: string, private _options: ManyToManyRelationNode, - private _model: ModelConstructorContract, + public model: ModelConstructorContract, ) { this._ensureRelatedModel() } @@ -122,10 +126,10 @@ export class ManyToMany implements RelationContract { * the keys validation, since they may be added after defining the relationship. */ private _validateKeys () { - const relationRef = `${this._model.name}.${this._relationName}` + const relationRef = `${this.model.name}.${this.relationName}` - if (!this._model.$hasColumn(this.localKey)) { - const ref = `${this._model.name}.${this.localKey}` + if (!this.model.$hasColumn(this.localKey)) { + const ref = `${this.model.name}.${this.localKey}` throw new Exception( `${ref} required by ${relationRef} relation is missing`, 500, @@ -143,49 +147,6 @@ export class ManyToMany implements RelationContract { } } - /** - * Raises exception when value for the foreign 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 - } - - /** - * Adds necessary select columns for the select query - */ - private _addSelect (query: ManyToManyQueryBuilderContract) { - query.select(`${this.relatedModel().$table}.*`) - query.pivotColumns( - [this.pivotForeignKey, this.pivotRelatedForeignKey].concat(this._options.pivotColumns || []), - ) - } - - /** - * Adds neccessary joins for the select query - */ - private _addJoin (query: ManyToManyQueryBuilderContract) { - query.innerJoin( - this.pivotTable, - `${this.relatedModel().$table}.${this.relatedAdapterKey}`, - `${this.pivotTable}.${this.pivotRelatedForeignKey}`, - ) - } - - /** - * Returns the belongs to query builder - */ - private _getQueryBuilder (client: QueryClientContract) { - return new ManyToManyQueryBuilder(client.knexQuery(), this, client) - } - /** * Compute keys */ @@ -195,15 +156,15 @@ export class ManyToMany implements RelationContract { } this.pivotTable = this._options.pivotTable || snakeCase( - sortBy([this.relatedModel().name, this._model.name]).join('_'), + sortBy([this.relatedModel().name, this.model.name]).join('_'), ) /** * Parent model and it's foreign key in pivot table */ - this.localKey = this._options.localKey || this._model.$primaryKey + this.localKey = this._options.localKey || this.model.$primaryKey this.pivotForeignKey = this._options.pivotForeignKey || snakeCase( - `${this._model.name}_${this._model.$primaryKey}`, + `${this.model.name}_${this.model.$primaryKey}`, ) this.pivotForeignKeyAlias = `pivot_${this.pivotForeignKey}` @@ -224,7 +185,7 @@ export class ManyToMany implements RelationContract { /** * Keys for the adapter */ - this.localAdapterKey = this._model.$getColumn(this.localKey)!.castAs + this.localAdapterKey = this.model.$getColumn(this.localKey)!.castAs this.relatedAdapterKey = this.relatedModel().$getColumn(this.relatedKey)!.castAs this.booted = true } @@ -233,13 +194,7 @@ export class ManyToMany implements RelationContract { * Must be implemented by main class */ public getQuery (parent: ModelContract, client: QueryClientContract): any { - const value = parent[this.localKey] - - const query = this._getQueryBuilder(client) - this._addSelect(query) - this._addJoin(query) - - return query.wherePivot(this.pivotForeignKey, value) + return new ManyToManyQueryBuilder(client.knexQuery(), this, client, parent) } /** @@ -247,15 +202,7 @@ export class ManyToMany implements RelationContract { * eagerloading */ public getEagerQuery (parents: ModelContract[], client: QueryClientContract): any { - const values = uniq(parents.map((parentInstance) => { - return this._ensureValue(parentInstance[this.localKey]) - })) - - const query = this._getQueryBuilder(client) - this._addSelect(query) - this._addJoin(query) - - return query.whereInPivot(this.pivotForeignKey, values) + return new ManyToManyQueryBuilder(client.knexQuery(), this, client, parents) } /** @@ -265,8 +212,7 @@ export class ManyToMany implements RelationContract { if (!related) { return } - - model.$setRelated(this._relationName as keyof typeof model, related) + model.$setRelated(this.relationName as keyof typeof model, related) } /** diff --git a/src/utils/index.ts b/src/utils/index.ts index 264e7c7b..13147e42 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -10,7 +10,7 @@ /// import { Exception } from '@poppinss/utils' -import { RelationContract } from '@ioc:Adonis/Lucid/Model' +import { RelationContract, ModelContract } from '@ioc:Adonis/Lucid/Model' /** * Ensure that relation is defined @@ -25,3 +25,25 @@ export function ensureRelation ( return true } + +/** + * Returns the value for a key from the model instance and raises descriptive + * exception when the value is missing + */ +export function getValue ( + model: ModelContract, + key: string, + relation: RelationContract, + action = 'preload', +) { + const value = model[key] + + if (value === undefined) { + throw new Exception( + `Cannot ${action} ${relation.relationName}, value of ${relation.model.name}.${key} is undefined`, + 500, + ) + } + + return value +} diff --git a/test-helpers/index.ts b/test-helpers/index.ts index 6297fd02..476bf658 100644 --- a/test-helpers/index.ts +++ b/test-helpers/index.ts @@ -33,7 +33,6 @@ import { import { ModelContract, AdapterContract, - RelationContract, ModelConstructorContract, ManyToManyQueryBuilderContract, } from '@ioc:Adonis/Lucid/Model' @@ -45,6 +44,7 @@ import { Database } from '../src/Database/index' import { RawQueryBuilder } from '../src/Database/QueryBuilder/Raw' import { InsertQueryBuilder } from '../src/Database/QueryBuilder/Insert' import { DatabaseQueryBuilder } from '../src/Database/QueryBuilder/Database' +import { ManyToMany } from '../src/Orm/Relations/ManyToMany/index' import { ManyToManyQueryBuilder } from '../src/Orm/Relations/ManyToMany/QueryBuilder' export const fs = new Filesystem(join(__dirname, 'tmp')) @@ -126,7 +126,7 @@ export async function setup () { if (!hasSkillsTable) { await db.schema.createTable('skills', (table) => { table.increments() - table.string('name') + table.string('name').notNullable() table.timestamps() }) } @@ -147,7 +147,7 @@ export async function setup () { await db.schema.createTable('posts', (table) => { table.increments() table.integer('user_id') - table.string('title') + table.string('title').notNullable() table.timestamps() }) } @@ -167,7 +167,7 @@ export async function setup () { await db.schema.createTable('profiles', (table) => { table.increments() table.integer('user_id') - table.string('display_name') + table.string('display_name').notNullable() table.timestamps() }) } @@ -240,11 +240,16 @@ export function getQueryBuilder (client: QueryClientContract) { /** * Returns many to many query builder */ -export function getManyToManyQueryBuilder (relation: RelationContract, client: QueryClientContract) { +export function getManyToManyQueryBuilder ( + parent: ModelContract, + relation: ManyToMany, + client: QueryClientContract, +) { return new ManyToManyQueryBuilder( client.getWriteClient().queryBuilder(), relation, client, + parent, ) as unknown as ManyToManyQueryBuilderContract & ExcutableQueryBuilderContract } diff --git a/test/orm/model-belongs-to.spec.ts b/test/orm/model-belongs-to.spec.ts index f421ca75..7db78dcb 100644 --- a/test/orm/model-belongs-to.spec.ts +++ b/test/orm/model-belongs-to.spec.ts @@ -223,6 +223,7 @@ test.group('Model | BelongsTo', (group) => { const { sql, bindings } = Profile.$getRelation('user')! .getEagerQuery([profile], Profile.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -260,6 +261,7 @@ test.group('Model | BelongsTo', (group) => { const { sql, bindings } = Profile.$getRelation('user')! .getQuery(profile, Profile.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -947,144 +949,285 @@ test.group('Model | BelongsTo | fetch related', (group) => { }) }) -// test.group('Model | BelongsTo | persist', (group) => { -// group.before(async () => { -// db = getDb() -// BaseModel = getBaseModel(ormAdapter(db)) -// await setup() -// }) +test.group('Model | HasOne | persist', (group) => { + group.before(async () => { + db = getDb() + BaseModel = getBaseModel(ormAdapter(db)) + await setup() + }) -// group.after(async () => { -// await cleanup() -// await db.manager.closeAll() -// }) + group.after(async () => { + await cleanup() + await db.manager.closeAll() + }) -// group.afterEach(async () => { -// await resetTables() -// }) + group.afterEach(async () => { + await resetTables() + }) -// test('save related instance', async (assert) => { -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + test('save related instance', async (assert) => { + class User extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public username: string -// } + @column() + public username: string + } -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public userId: number + @column() + public userId: number -// @column() -// public displayName: string + @column() + public displayName: string -// @belongsTo(() => User) -// public user: User -// } + @belongsTo(() => User) + public user: User + } -// const profile = new Profile() -// profile.displayName = 'virk' -// await profile.save() + const user = new User() + user.username = 'virk' + await user.save() -// const user = new User() -// user.username = 'virk' + const profile = new Profile() + profile.displayName = 'Hvirk' -// await profile.associate('user', user) + await profile.related<'belongsTo', 'user'>('user').associate(user) -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) -// }) + assert.isTrue(profile.$persisted) + assert.equal(user.id, profile.userId) + }) -// test('use parent model transaction when defined', async (assert) => { -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + test('wrap save calls inside transaction', async (assert) => { + assert.plan(5) -// @column() -// public username: string -// } + class User extends BaseModel { + @column({ primary: true }) + public id: number -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + @column() + public username: string + } -// @column() -// public userId: number + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public displayName: string + @column() + public userId: number -// @belongsTo(() => User) -// public user: User -// } + @column() + public displayName: string -// const profile = new Profile() -// profile.displayName = 'virk' -// await profile.save() + @belongsTo(() => User) + public user: User + } + + const user = new User() + const profile = new Profile() + + try { + await profile.related<'belongsTo', 'user'>('user').associate(user) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) -// const user = new User() -// user.username = 'virk' + test('do not wrap when wrapInTransaction is set to false', async (assert) => { + assert.plan(5) -// const trx = await db.transaction() -// profile.$trx = trx -// await profile.associate('user', user) + class User extends BaseModel { + @column({ primary: true }) + public id: number -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) + @column() + public username: string + } -// await trx.rollback() + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// const totalUsers = await db.from('users').count('*', 'total') -// const profiles = await db.from('profiles') + @column() + public userId: number -// assert.lengthOf(profiles, 1) -// assert.equal(profiles[0].user_id, null) + @column() + public displayName: string -// assert.equal(totalUsers[0].total, 0) -// assert.isUndefined(user.$trx) -// assert.isUndefined(profile.$trx) -// }) + @belongsTo(() => User) + public user: User + } -// test('use parent model options when defined', async (assert) => { -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + const user = new User() + user.username = 'virk' -// @column() -// public username: string -// } + const profile = new Profile() -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + try { + await profile.related<'belongsTo', 'user'>('user').associate(user, false) + } catch (error) { + assert.exists(error) + } -// @column() -// public userId: number + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') -// @column() -// public displayName: string + assert.equal(totalUsers[0].total, 1) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) -// @belongsTo(() => User) -// public user: User -// } + test('do not wrap in transaction when parent has been persisted', async (assert) => { + assert.plan(5) -// const profile = new Profile() -// profile.displayName = 'virk' -// profile.$options = { connection: 'secondary' } -// await profile.save() + class User extends BaseModel { + @column({ primary: true }) + public id: number -// const user = new User() -// user.username = 'virk' -// await profile.associate('user', user) + @column() + public username: string + } -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// assert.deepEqual(user.$options, { connection: 'secondary' }) -// assert.deepEqual(profile.$options, { connection: 'secondary' }) -// }) -// }) + @column() + public userId: number + + @column() + public displayName: string + + @belongsTo(() => User) + public user: User + } + + const user = new User() + user.username = 'virk' + await user.save() + + const profile = new Profile() + + try { + await profile.related<'belongsTo', 'user'>('user').associate(user) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) + + test('use parent model transaction when defined', async (assert) => { + assert.plan(4) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + class Profile extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public displayName: string + + @belongsTo(() => User) + public user: User + } + + const trx = await db.transaction() + + const user = new User() + user.username = 'virk' + + const profile = new Profile() + profile.$trx = trx + profile.displayName = 'virk' + + await profile.related<'belongsTo', 'user'>('user').associate(user) + await trx.rollback() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) + + test('create save point when parent is already in transaction', async (assert) => { + assert.plan(5) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + class Profile extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public displayName: string + + @belongsTo(() => User) + public user: User + } + + const trx = await db.transaction() + + const user = new User() + user.username = 'virk' + + const profile = new Profile() + profile.$trx = trx + + try { + await profile.related<'belongsTo', 'user'>('user').associate(user) + } catch (error) { + assert.exists(error) + } + + await trx.commit() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) +}) diff --git a/test/orm/model-has-many-through.spec.ts b/test/orm/model-has-many-through.spec.ts index 57e646d9..0374a35b 100644 --- a/test/orm/model-has-many-through.spec.ts +++ b/test/orm/model-has-many-through.spec.ts @@ -189,6 +189,7 @@ test.group('Model | Has Many Through', (group) => { const { sql, bindings } = Country.$getRelation('posts')! .getQuery(country, Country.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -237,6 +238,7 @@ test.group('Model | Has Many Through', (group) => { const { sql, bindings } = Country.$getRelation('posts')! .getEagerQuery([country], Country.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() diff --git a/test/orm/model-has-many.spec.ts b/test/orm/model-has-many.spec.ts index b88f001d..2f363727 100644 --- a/test/orm/model-has-many.spec.ts +++ b/test/orm/model-has-many.spec.ts @@ -194,6 +194,7 @@ test.group('Model | HasMany', (group) => { const { sql, bindings } = User.$getRelation('posts')! .getEagerQuery([user], User.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -231,6 +232,7 @@ test.group('Model | HasMany', (group) => { const { sql, bindings } = User.$getRelation('posts')! .getQuery(user, User.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -986,178 +988,591 @@ test.group('Model | HasMany | fetch related', (group) => { }) }) -// test.group('Model | HasMany | persist', (group) => { -// group.before(async () => { -// db = getDb() -// BaseModel = getBaseModel(ormAdapter(db)) -// await setup() -// }) +test.group('Model | HasMany | persist', (group) => { + group.before(async () => { + db = getDb() + BaseModel = getBaseModel(ormAdapter(db)) + await setup() + }) + + group.after(async () => { + await cleanup() + await db.manager.closeAll() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('save related instance', async (assert) => { + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const post = new Post() + post.title = 'Adonis 101' + + await user.related('posts').save(post) + + assert.isTrue(post.$persisted) + assert.equal(user.id, post.userId) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 1) + }) + + test('save many related instance', async (assert) => { + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const post = new Post() + post.title = 'Adonis 101' + + const post1 = new Post() + post1.title = 'Lucid 101' + + await user.related('posts').saveMany([post, post1]) + + assert.isTrue(post.$persisted) + assert.equal(user.id, post.userId) + + assert.isTrue(post1.$persisted) + assert.equal(user.id, post1.userId) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 2) + }) + + test('wrap save calls inside transaction', async (assert) => { + assert.plan(5) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + + const post = new Post() + + try { + await user.related('posts').save(post) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) + + test('wrap save many calls inside transaction', async (assert) => { + assert.plan(6) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + + const post = new Post() + post.title = 'Adonis 101' + + const post1 = new Post() + + try { + await user.related('posts').saveMany([post, post1]) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + assert.isUndefined(post1.$trx) + }) + + test('do not wrap when wrapInTransaction is set to false', async (assert) => { + assert.plan(5) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + + const post = new Post() + + try { + await user.related('posts').save(post, false) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) + + test('do not wrap with saveMany when wrapInTransaction is set to false', async (assert) => { + assert.plan(5) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + + const post = new Post() + post.title = 'Adonis 101' + + const post1 = new Post() + + try { + await user.related('posts').saveMany([post, post1], false) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 1) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) + + test('do not wrap in transaction when parent has been persisted', async (assert) => { + assert.plan(5) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const post = new Post() + + try { + await user.related('posts').save(post) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) + + test('do wrap in transaction with saveMany even when parent has been persisted', async (assert) => { + assert.plan(6) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const post = new Post() + post.title = 'Adonis 101' + + const post1 = new Post() + + try { + await user.related('posts').saveMany([post, post1]) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') -// group.after(async () => { -// await cleanup() -// await db.manager.closeAll() -// }) + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + assert.isUndefined(post1.$trx) + }) + + test('use parent model transaction when defined', async (assert) => { + assert.plan(4) -// group.afterEach(async () => { -// await resetTables() -// }) + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number -// test('save related instance', async (assert) => { -// class Post extends BaseModel { -// @column({ primary: true }) -// public id: number + @column() + public title: string + } -// @column() -// public userId: number + class User extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public title: string -// } + @column() + public username: string -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + @hasMany(() => Post) + public posts: Post[] + } -// @column() -// public username: string + const trx = await db.transaction() -// @hasMany(() => Post) -// public posts: Post[] -// } + const user = new User() + user.username = 'virk' + user.$trx = trx + await user.save() -// const user = new User() -// user.username = 'virk' -// await user.save() + const post = new Post() + post.title = 'Adonis 101' -// const post = new Post() -// post.title = 'Hvirk' + await user.related('posts').save(post) + await trx.rollback() -// await user.saveRelated('posts', post) + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') -// assert.isTrue(post.$persisted) -// assert.equal(user.id, post.userId) -// }) + assert.equal(totalUsers[0].total, 0) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) + + test('use parent model transaction with save many when defined', async (assert) => { + assert.plan(5) + + class Post extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public title: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasMany(() => Post) + public posts: Post[] + } -// test('use parent model transaction when defined', async (assert) => { -// class Post extends BaseModel { -// @column({ primary: true }) -// public id: number + const trx = await db.transaction() -// @column() -// public userId: number + const user = new User() + user.username = 'virk' + user.$trx = trx + await user.save() -// @column() -// public title: string -// } + const post = new Post() + post.title = 'Adonis 101' -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + const post1 = new Post() + post1.title = 'Lucid 101' -// @column() -// public username: string + await user.related('posts').saveMany([post, post1]) + await trx.rollback() -// @hasMany(() => Post) -// public posts: Post[] -// } + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') -// const trx = await db.transaction() + assert.equal(totalUsers[0].total, 0) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + assert.isUndefined(post1.$trx) + }) -// const user = new User() -// user.username = 'virk' -// user.$trx = trx + test('create save point when parent is already in transaction', async (assert) => { + assert.plan(5) -// await user.save() + class Post extends BaseModel { + @column({ primary: true }) + public id: number -// const post = new Post() -// post.title = 'Hvirk' + @column() + public userId: number -// await user.saveRelated('posts', post) -// assert.isTrue(post.$persisted) -// assert.equal(user.id, post.userId) + @column() + public title: string + } -// await trx.rollback() -// const totalUsers = await db.from('users').count('*', 'total') -// const totalPosts = await db.from('posts').count('*', 'total') + class User extends BaseModel { + @column({ primary: true }) + public id: number -// assert.equal(totalPosts[0].total, 0) -// assert.equal(totalUsers[0].total, 0) -// assert.isUndefined(user.$trx) -// assert.isUndefined(post.$trx) -// }) + @column() + public username: string -// test('use parent model options when defined', async (assert) => { -// class Post extends BaseModel { -// @column({ primary: true }) -// public id: number + @hasMany(() => Post) + public posts: Post[] + } -// @column() -// public userId: number + const trx = await db.transaction() -// @column() -// public title: string -// } + const user = new User() + user.username = 'virk' + user.$trx = trx -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + const post = new Post() -// @column() -// public username: string + try { + await user.related('posts').save(post) + } catch (error) { + assert.exists(error) + } + await trx.commit() -// @hasMany(() => Post) -// public posts: Post[] -// } + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') -// const user = new User() -// user.username = 'virk' -// user.$options = { connection: 'secondary' } -// await user.save() + assert.equal(totalUsers[0].total, 0) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) -// const post = new Post() -// post.title = 'Hvirk' + test('create save point with saveMany when parent is already in transaction', async (assert) => { + assert.plan(5) -// await user.saveRelated('posts', post) + class Post extends BaseModel { + @column({ primary: true }) + public id: number -// assert.isTrue(post.$persisted) -// assert.equal(user.id, post.userId) + @column() + public userId: number -// assert.deepEqual(user.$options, { connection: 'secondary' }) -// assert.deepEqual(post.$options, { connection: 'secondary' }) -// }) + @column() + public title: string + } -// test('persist parent model when not already persisted', async (assert) => { -// class Post extends BaseModel { -// @column({ primary: true }) -// public id: number + class User extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public userId: number + @column() + public username: string -// @column() -// public title: string -// } + @hasMany(() => Post) + public posts: Post[] + } -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + const trx = await db.transaction() -// @column() -// public username: string + const user = new User() + user.username = 'virk' + user.$trx = trx -// @hasMany(() => Post) -// public posts: Post[] -// } + const post = new Post() + post.title = 'Adonis 101' -// const user = new User() -// user.username = 'virk' + const post1 = new Post() -// const post = new Post() -// post.title = 'Hvirk' + try { + await user.related('posts').saveMany([post, post1]) + } catch (error) { + assert.exists(error) + } + await trx.commit() -// await user.saveRelated('posts', post) + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('posts').count('*', 'total') -// assert.isTrue(post.$persisted) -// assert.equal(user.id, post.userId) -// }) -// }) + assert.equal(totalUsers[0].total, 0) + assert.equal(totalPosts[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(post.$trx) + }) +}) diff --git a/test/orm/model-has-one.spec.ts b/test/orm/model-has-one.spec.ts index 7e3a5d21..7c6c8a99 100644 --- a/test/orm/model-has-one.spec.ts +++ b/test/orm/model-has-one.spec.ts @@ -198,6 +198,7 @@ test.group('Model | HasOne | preload', (group) => { const { sql, bindings } = User.$getRelation('profile')! .getEagerQuery([user], User.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -235,6 +236,7 @@ test.group('Model | HasOne | preload', (group) => { const { sql, bindings } = User.$getRelation('profile')! .getQuery(user, User.query().client) + .applyConstraints() .toSQL() const { sql: knexSql, bindings: knexBindings } = db.query() @@ -933,176 +935,288 @@ test.group('Model | HasOne | fetch related', (group) => { }) }) -// test.group('Model | HasOne | persist', (group) => { -// group.before(async () => { -// db = getDb() -// BaseModel = getBaseModel(ormAdapter(db)) -// await setup() -// }) +test.group('Model | HasOne | persist', (group) => { + group.before(async () => { + db = getDb() + BaseModel = getBaseModel(ormAdapter(db)) + await setup() + }) + + group.after(async () => { + await cleanup() + await db.manager.closeAll() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('save related instance', async (assert) => { + class Profile extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public displayName: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasOne(() => Profile) + public profile: Profile + } + + const user = new User() + user.username = 'virk' + await user.save() + + const profile = new Profile() + profile.displayName = 'Hvirk' + + await user.related<'hasOne', 'profile'>('profile').save(profile) + + assert.isTrue(profile.$persisted) + assert.equal(user.id, profile.userId) + }) + + test('wrap save calls inside transaction', async (assert) => { + assert.plan(5) + + class Profile extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public userId: number + + @column() + public displayName: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @hasOne(() => Profile) + public profile: Profile + } + + const user = new User() + user.username = 'virk' + + const profile = new Profile() + + try { + await user.related<'hasOne', 'profile'>('profile').save(profile) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) + + test('do not wrap when wrapInTransaction is set to false', async (assert) => { + assert.plan(5) + + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// group.after(async () => { -// await cleanup() -// await db.manager.closeAll() -// }) + @column() + public userId: number -// group.afterEach(async () => { -// await resetTables() -// }) + @column() + public displayName: string + } -// test('save related instance', async (assert) => { -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + class User extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public userId: number + @column() + public username: string -// @column() -// public displayName: string -// } + @hasOne(() => Profile) + public profile: Profile + } -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + const user = new User() + user.username = 'virk' -// @column() -// public username: string + const profile = new Profile() -// @hasOne(() => Profile) -// public profile: Profile -// } + try { + await user.related<'hasOne', 'profile'>('profile').save(profile, false) + } catch (error) { + assert.exists(error) + } -// const user = new User() -// user.username = 'virk' -// await user.save() + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') -// const profile = new Profile() -// profile.displayName = 'Hvirk' + assert.equal(totalUsers[0].total, 1) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) -// await user.saveRelated('profile', profile) + test('do not wrap in transaction when parent has been persisted', async (assert) => { + assert.plan(5) -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) -// }) + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// test('use parent model transaction when defined', async (assert) => { -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + @column() + public userId: number -// @column() -// public userId: number + @column() + public displayName: string + } -// @column() -// public displayName: string -// } + class User extends BaseModel { + @column({ primary: true }) + public id: number -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + @column() + public username: string -// @column() -// public username: string + @hasOne(() => Profile) + public profile: Profile + } -// @hasOne(() => Profile) -// public profile: Profile -// } + const user = new User() + user.username = 'virk' + await user.save() -// const trx = await db.transaction() + const profile = new Profile() -// const user = new User() -// user.username = 'virk' -// user.$trx = trx + try { + await user.related<'hasOne', 'profile'>('profile').save(profile) + } catch (error) { + assert.exists(error) + } -// await user.save() + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') -// const profile = new Profile() -// profile.displayName = 'Hvirk' + assert.equal(totalUsers[0].total, 1) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) -// await user.saveRelated('profile', profile) -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) + test('use parent model transaction when defined', async (assert) => { + assert.plan(4) -// await trx.rollback() -// const totalUsers = await db.from('users').count('*', 'total') -// const totalProfiles = await db.from('profiles').count('*', 'total') + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// assert.equal(totalProfiles[0].total, 0) -// assert.equal(totalUsers[0].total, 0) -// assert.isUndefined(user.$trx) -// assert.isUndefined(profile.$trx) -// }) + @column() + public userId: number -// test('use parent model options when defined', async (assert) => { -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + @column() + public displayName: string + } -// @column() -// public userId: number + class User extends BaseModel { + @column({ primary: true }) + public id: number -// @column() -// public displayName: string -// } + @column() + public username: string -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + @hasOne(() => Profile) + public profile: Profile + } + + const trx = await db.transaction() + + const user = new User() + user.username = 'virk' + user.$trx = trx + await user.save() + + const profile = new Profile() + profile.displayName = 'virk' -// @column() -// public username: string + await user.related<'hasOne', 'profile'>('profile').save(profile) + await trx.rollback() -// @hasOne(() => Profile) -// public profile: Profile -// } + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') -// const user = new User() -// user.username = 'virk' -// user.$options = { connection: 'secondary' } -// await user.save() + assert.equal(totalUsers[0].total, 0) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) + + test('create save point when parent is already in transaction', async (assert) => { + assert.plan(5) + + class Profile extends BaseModel { + @column({ primary: true }) + public id: number -// const profile = new Profile() -// profile.displayName = 'Hvirk' -// await user.saveRelated('profile', profile) + @column() + public userId: number -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) + @column() + public displayName: string + } -// assert.deepEqual(user.$options, { connection: 'secondary' }) -// assert.deepEqual(profile.$options, { connection: 'secondary' }) -// }) + class User extends BaseModel { + @column({ primary: true }) + public id: number -// test('persist parent model when not already persisted', async (assert) => { -// class Profile extends BaseModel { -// @column({ primary: true }) -// public id: number + @column() + public username: string -// @column() -// public userId: number + @hasOne(() => Profile) + public profile: Profile + } -// @column() -// public displayName: string -// } + const trx = await db.transaction() -// class User extends BaseModel { -// @column({ primary: true }) -// public id: number + const user = new User() + user.username = 'virk' + user.$trx = trx -// @column() -// public username: string + const profile = new Profile() -// @hasOne(() => Profile) -// public profile: Profile -// } + try { + await user.related<'hasOne', 'profile'>('profile').save(profile) + } catch (error) { + assert.exists(error) + } -// const user = new User() -// user.username = 'virk' + await trx.commit() -// const profile = new Profile() -// profile.displayName = 'Hvirk' -// await user.saveRelated('profile', profile) + const totalUsers = await db.query().from('users').count('*', 'total') + const totalProfiles = await db.query().from('profiles').count('*', 'total') -// assert.isTrue(profile.$persisted) -// assert.equal(user.id, profile.userId) -// }) -// }) + assert.equal(totalUsers[0].total, 0) + assert.equal(totalProfiles[0].total, 0) + assert.isUndefined(user.$trx) + assert.isUndefined(profile.$trx) + }) +}) diff --git a/test/orm/model-many-to-many.spec.ts b/test/orm/model-many-to-many.spec.ts index 22cc8821..337db6a2 100644 --- a/test/orm/model-many-to-many.spec.ts +++ b/test/orm/model-many-to-many.spec.ts @@ -10,6 +10,7 @@ /// import test from 'japa' +import { ManyToMany } from '../../src/Orm/Relations/ManyToMany' import { ManyToManyQueryBuilder } from '../../src/Orm/Relations/ManyToMany/QueryBuilder' import { manyToMany, column } from '../../src/Orm/Decorators' @@ -281,7 +282,11 @@ test.group('Model | Many To Many', (group) => { const user = new User() user.id = 1 - const { sql, bindings } = User.$getRelation('skills')!.getEagerQuery([user], User.query().client).toSQL() + const { sql, bindings } = User.$getRelation('skills')! + .getEagerQuery([user], User.query().client) + .applyConstraints() + .toSQL() + const { sql: knexSql, bindings: knexBindings } = db.query() .from('skills') .select([ @@ -317,7 +322,11 @@ test.group('Model | Many To Many', (group) => { const user = new User() user.id = 1 - const { sql, bindings } = User.$getRelation('skills')!.getQuery(user, User.query().client).toSQL() + const { sql, bindings } = User.$getRelation('skills')! + .getQuery(user, User.query().client) + .applyConstraints() + .toSQL() + const { sql: knexSql, bindings: knexBindings } = db.query() .from('skills') .select([ @@ -795,7 +804,7 @@ test.group('ManyToMany Query Builder | where', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .wherePivot('username', 'virk') @@ -829,7 +838,7 @@ test.group('ManyToMany Query Builder | where', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .where((builder) => builder.wherePivot('username', 'virk')) @@ -863,7 +872,7 @@ test.group('ManyToMany Query Builder | where', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .wherePivot('age', '>', 22) @@ -897,7 +906,7 @@ test.group('ManyToMany Query Builder | where', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .wherePivot('age', '>', db.raw('select min_age from ages limit 1;')) @@ -935,7 +944,7 @@ test.group('ManyToMany Query Builder | where', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .wherePivot('age', '>', 22) @@ -971,7 +980,7 @@ test.group('ManyToMany Query Builder | where', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .wherePivot('age', '>', 22) @@ -1024,7 +1033,7 @@ test.group('ManyToMany Query Builder | whereNot', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotPivot('username', 'virk') @@ -1058,7 +1067,7 @@ test.group('ManyToMany Query Builder | whereNot', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotPivot('age', '>', 22) @@ -1092,7 +1101,7 @@ test.group('ManyToMany Query Builder | whereNot', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotPivot('age', '>', db.raw('select min_age from ages limit 1;')) @@ -1130,7 +1139,7 @@ test.group('ManyToMany Query Builder | whereNot', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotPivot('age', '>', 22) @@ -1179,7 +1188,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot('username', ['virk', 'nikk']) @@ -1213,7 +1222,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot('username', (builder) => { @@ -1251,7 +1260,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot('username', db.query().select('id').from('accounts')) @@ -1287,7 +1296,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot('username', [ @@ -1325,7 +1334,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot( @@ -1365,7 +1374,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot(['username', 'email'], [['foo', 'bar']]) @@ -1399,7 +1408,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot('username', ['virk', 'nikk']) @@ -1435,7 +1444,7 @@ test.group('ManyToMany Query Builder | whereIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereInPivot('username', (builder) => { @@ -1492,7 +1501,7 @@ test.group('ManyToMany Query Builder | whereNotIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotInPivot('username', ['virk', 'nikk']) @@ -1526,7 +1535,7 @@ test.group('ManyToMany Query Builder | whereNotIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotInPivot('username', (builder) => { @@ -1564,7 +1573,7 @@ test.group('ManyToMany Query Builder | whereNotIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotInPivot('username', db.query().select('username').from('accounts')) @@ -1601,7 +1610,7 @@ test.group('ManyToMany Query Builder | whereNotIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotInPivot(['username', 'email'], [['foo', 'bar']]) @@ -1635,7 +1644,7 @@ test.group('ManyToMany Query Builder | whereNotIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotInPivot('username', ['virk', 'nikk']) @@ -1671,7 +1680,7 @@ test.group('ManyToMany Query Builder | whereNotIn', (group) => { const connection = db.connection() const relation = User.$getRelation('skills')! - const query = getManyToManyQueryBuilder(relation, connection) + const query = getManyToManyQueryBuilder(new User(), relation as ManyToMany, connection) const { sql, bindings } = query .whereNotInPivot('username', (builder) => { @@ -1817,3 +1826,982 @@ test.group('Model | ManyToMany | fetch', (group) => { assert.equal(skills[1].$extras.pivot_skill_id, 2) }) }) + +test.group('Model | ManyToMany | persist', (group) => { + group.before(async () => { + db = getDb() + BaseModel = getBaseModel(ormAdapter(db)) + await setup() + }) + + group.after(async () => { + await cleanup() + await db.manager.closeAll() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('save related instance', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill) + + assert.isTrue(user.$persisted) + assert.isTrue(skill.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 1) + + assert.lengthOf(skillUsers, 1) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + + test('attach duplicates when save is called twice', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill) + await user.related<'manyToMany', 'skills'>('skills').save(skill, true, false) + + assert.isTrue(user.$persisted) + assert.isTrue(skill.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 1) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill.id) + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + + test('do not attach duplicates when checkExisting is true', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill) + await user.related<'manyToMany', 'skills'>('skills').save(skill) + + assert.isTrue(user.$persisted) + assert.isTrue(skill.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalPosts = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalPosts[0].total, 1) + + assert.lengthOf(skillUsers, 1) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + + test('attach when related pivot entry exists but for a different parent', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const user1 = new User() + user1.username = 'nikk' + await user1.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill) + await user1.related<'manyToMany', 'skills'>('skills').save(skill) + + assert.isTrue(user.$persisted) + assert.isTrue(skill.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 2) + assert.equal(totalSkills[0].total, 1) + + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + + assert.equal(skillUsers[1].user_id, user1.id) + assert.equal(skillUsers[1].skill_id, skill.id) + + assert.isUndefined(user.$trx) + assert.isUndefined(user1.$trx) + assert.isUndefined(skill.$trx) + }) + + test('save many of related instance', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Dancing' + + await user.related('skills').saveMany([skill, skill1]) + + assert.isTrue(user.$persisted) + assert.isTrue(skill.$persisted) + assert.isTrue(skill1.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalSkills[0].total, 2) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill1.id) + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + + test('save many add duplicates when checkExisting is false', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Dancing' + + await user.related('skills').save(skill) + await user.related<'manyToMany', 'skills'>('skills').saveMany([skill, skill1], true, false) + + assert.isTrue(user.$persisted) + assert.isTrue(skill.$persisted) + assert.isTrue(skill1.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalSkills[0].total, 2) + + assert.lengthOf(skillUsers, 3) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill.id) + + assert.equal(skillUsers[2].user_id, user.id) + assert.equal(skillUsers[2].skill_id, skill1.id) + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + + test('wrap calls inside transaction', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + + const skill = new Skill() + + try { + await user.related('skills').save(skill) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('wrap calls inside transaction even when parent has been persisted', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + + try { + await user.related('skills').save(skill) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('do not wrap calls inside transaction when wrapInTransaction=false', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + + const skill = new Skill() + + try { + await user.related('skills').save(skill, false) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('wrap save many calls inside transaction', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + + try { + await user.related('skills').saveMany([skill, skill1]) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('do not wrap save many calls inside transaction when wrapInTransaction=false', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + + try { + await user.related('skills').saveMany([skill, skill1], false) + } catch (error) { + assert.exists(error) + } + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + assert.equal(totalSkills[0].total, 1) + assert.lengthOf(skillUsers, 0) + }) + + test('use parent model transaction when defined', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const trx = await db.transaction() + + const user = new User() + user.$trx = trx + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill) + await trx.rollback() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('use parent model transaction when wrapInTransaction=false', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const trx = await db.transaction() + + const user = new User() + user.$trx = trx + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill, false) + await trx.rollback() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('use parent model transaction with save many when defined', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const trx = await db.transaction() + + const user = new User() + user.$trx = trx + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Dancy' + + await user.related('skills').saveMany([skill, skill1]) + await trx.rollback() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('use parent model transaction with save many when wrapInTransaction=false', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const trx = await db.transaction() + + const user = new User() + user.$trx = trx + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Dancing' + + await user.related('skills').saveMany([skill, skill1], false) + await trx.rollback() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('create save point when parent is already in transaction', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const trx = await db.transaction() + + const user = new User() + user.$trx = trx + user.username = 'virk' + + const skill = new Skill() + + try { + await user.related('skills').save(skill) + } catch (error) { + assert.exists(error) + } + + await trx.commit() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) + + test('create save point with save many when parent is already in transaction', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const trx = await db.transaction() + + const user = new User() + user.$trx = trx + user.username = 'virk' + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + + try { + await user.related('skills').saveMany([skill, skill1]) + } catch (error) { + assert.exists(error) + } + + await trx.commit() + + const totalUsers = await db.query().from('users').count('*', 'total') + const totalSkills = await db.query().from('skills').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 0) + assert.equal(totalSkills[0].total, 0) + assert.lengthOf(skillUsers, 0) + }) +}) + +test.group('Model | ManyToMany | attach', (group) => { + group.before(async () => { + db = getDb() + BaseModel = getBaseModel(ormAdapter(db)) + await setup() + }) + + group.after(async () => { + await cleanup() + await db.manager.closeAll() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('attach pivot ids', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + await user.related<'manyToMany', 'skills'>('skills').attach([1, 2]) + assert.isTrue(user.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, 1) + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, 2) + + assert.isUndefined(user.$trx) + }) + + test('attach pivot ids avoid duplicates', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + await user.related<'manyToMany', 'skills'>('skills').attach([1, 1, 2]) + assert.isTrue(user.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, 1) + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, 2) + + assert.isUndefined(user.$trx) + }) + + test('fail attach when parent model has not been persisted', async (assert) => { + assert.plan(1) + + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + + try { + await user.related<'manyToMany', 'skills'>('skills').attach([1, 1, 2]) + } catch ({ message }) { + assert.equal(message, 'Cannot attach skills, value of User.id is undefined') + } + }) + + test('attach with extra data', async (assert) => { + class Skill extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: Skill[] + } + + const user = new User() + user.username = 'virk' + await user.save() + + await user.related<'manyToMany', 'skills'>('skills').attach({ + 1: { proficiency: 'Master' }, + 2: { proficiency: 'Beginner' }, + }) + assert.isTrue(user.$persisted) + + const totalUsers = await db.query().from('users').count('*', 'total') + const skillUsers = await db.query().from('skill_user') + + assert.equal(totalUsers[0].total, 1) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, 1) + assert.equal(skillUsers[0].proficiency, 'Master') + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, 2) + assert.equal(skillUsers[1].proficiency, 'Beginner') + + assert.isUndefined(user.$trx) + }) +})