From 8a22b855439808be1f8bc90825bdeb7cbde19570 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 26 Apr 2021 00:25:04 +0530 Subject: [PATCH] feat: add support for passing custom pivot attributes during create/save calls --- adonis-typings/relations.ts | 22 +- src/Orm/Relations/ManyToMany/QueryClient.ts | 78 ++- test/orm/model-many-to-many.spec.ts | 515 ++++++++++++++++++++ 3 files changed, 586 insertions(+), 29 deletions(-) diff --git a/adonis-typings/relations.ts b/adonis-typings/relations.ts index 2d557d51..6f3048b9 100644 --- a/adonis-typings/relations.ts +++ b/adonis-typings/relations.ts @@ -700,32 +700,42 @@ declare module '@ioc:Adonis/Lucid/Relations' { /** * Save related model instance. Sets up FK automatically */ - save(related: InstanceType, checkExisting?: boolean): Promise + save( + related: InstanceType, + performSync?: boolean, // defaults to true + pivotAttributes?: ModelObject + ): Promise /** * Save many of related model instance. Sets up FK automatically */ - saveMany(related: InstanceType[], checkExisting?: boolean): Promise + saveMany( + related: InstanceType[], + performSync?: boolean, // defaults to true + pivotAttributes?: (ModelObject | undefined)[] + ): Promise /** * Create related model instance. Sets up FK automatically */ create( - values: Partial>> + values: Partial>>, + pivotAttributes?: ModelObject ): Promise> /** * Create many of related model instances. Sets up FK automatically */ createMany( - values: Partial>>[] + values: Partial>>[], + pivotAttributes?: (ModelObject | undefined)[] ): Promise[]> /** * Attach new pivot rows */ attach( - ids: (string | number)[] | { [key: string]: ModelObject }, + ids: (string | number)[] | Record, trx?: TransactionClientContract ): Promise @@ -738,7 +748,7 @@ declare module '@ioc:Adonis/Lucid/Relations' { * Sync pivot rows. */ sync( - ids: (string | number)[] | { [key: string]: ModelObject }, + ids: (string | number)[] | Record, detach?: boolean, trx?: TransactionClientContract ): Promise diff --git a/src/Orm/Relations/ManyToMany/QueryClient.ts b/src/Orm/Relations/ManyToMany/QueryClient.ts index fa5debf2..00d1a3e5 100644 --- a/src/Orm/Relations/ManyToMany/QueryClient.ts +++ b/src/Orm/Relations/ManyToMany/QueryClient.ts @@ -123,7 +123,7 @@ export class ManyToManyQueryClient implements ManyToManyClientContract { /** * Persist parent @@ -137,15 +137,19 @@ export class ManyToManyQueryClient implements ManyToManyClientContract { /** * Persist parent @@ -170,14 +178,20 @@ export class ManyToManyQueryClient implements ManyToManyClientContract>( + (result, one, index) => { + const [, relatedForeignKeyValue] = this.relation.getPivotRelatedPair(one) + result[relatedForeignKeyValue] = pivotAttributes?.[index] || {} + return result + }, + {} + ) + /** * Sync when checkExisting = true, to avoid duplicate rows. Otherwise * perform insert */ - const relatedForeignKeyValues = related.map( - (one) => this.relation.getPivotRelatedPair(one)[1] - ) - if (checkExisting) { + if (performSync) { await this.sync(relatedForeignKeyValues, false, trx) } else { await this.attach(relatedForeignKeyValues, trx) @@ -190,7 +204,7 @@ export class ManyToManyQueryClient implements ManyToManyClientContract { + public async create(values: ModelObject, pivotAttributes?: ModelObject): Promise { return managedTransaction(this.parent.$trx || this.client, async (trx) => { this.parent.$trx = trx await this.parent.save() @@ -200,12 +214,15 @@ export class ManyToManyQueryClient implements ManyToManyClientContract { + public async createMany( + values: ModelObject[], + pivotAttributes?: (ModelObject | undefined)[] + ): Promise { return managedTransaction(this.parent.$trx || this.client, async (trx) => { this.parent.$trx = trx await this.parent.save() @@ -225,12 +245,18 @@ export class ManyToManyQueryClient implements ManyToManyClientContract>( + (result, one, index) => { + const [, relatedForeignKeyValue] = this.relation.getPivotRelatedPair(one) + result[relatedForeignKeyValue] = pivotAttributes?.[index] || {} + return result + }, + {} + ) + /** - * Sync or attach new rows + * Attach new rows */ - const relatedForeignKeyValues = related.map( - (one) => this.relation.getPivotRelatedPair(one)[1] - ) await this.attach(relatedForeignKeyValues, trx) return related }) @@ -310,6 +336,10 @@ export class ManyToManyQueryClient implements ManyToManyClientContract { result[id] = {} return result - }, {}) - : ids + }, {} as Record) + : (ids as Record) const query = this.pivotQuery().useTransaction(transaction) /** * We must scope the select query to related foreign key when ids * is an array and not an object. This will help in performance - * when their are indexes defined on this key + * when there are indexes defined on this key */ if (!hasAttributes) { query.select(this.relation.pivotRelatedForeignKey) @@ -359,6 +389,8 @@ export class ManyToManyQueryClient implements ManyToManyClientContract { assert.isUndefined(skill.$trx) }) + test('save related instance with pivot attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill, true, { + proficiency: 'Master', + }) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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.equal(skillUsers[0].proficiency, 'Master') + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + test('do not attach duplicates when save is called more than once', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true }) @@ -4250,6 +4299,59 @@ test.group('Model | ManyToMany | save', (group) => { assert.isUndefined(skill.$trx) }) + test('perform update with different pivot attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill, true, { + proficiency: 'Master', + }) + await user.related('skills').save(skill, true, { + proficiency: 'Beginner', + }) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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.equal(skillUsers[0].proficiency, 'Beginner') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + test('attach duplicates when save is called more than once with with checkExisting = false', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true }) @@ -4301,6 +4403,63 @@ test.group('Model | ManyToMany | save', (group) => { assert.isUndefined(skill.$trx) }) + test('attach duplicates with different pivot attributes and with checkExisting = false', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + await user.related('skills').save(skill, true, { + proficiency: 'Master', + }) + await user.related('skills').save(skill, false, { + proficiency: 'Beginner', + }) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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').orderBy('id', 'desc') + + 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[0].proficiency, 'Beginner') + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill.id) + assert.equal(skillUsers[1].proficiency, 'Master') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + test('attach when related pivot entry exists but for a different parent @sanityCheck', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true }) @@ -4428,6 +4587,130 @@ test.group('Model | ManyToMany | saveMany', (group) => { assert.isUndefined(skill1.$trx) }) + test('save many of related instance with pivot attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Cooking' + + await user.related('skills').saveMany([skill, skill1], true, [ + { + proficiency: 'Master', + }, + { + proficiency: 'Beginner', + }, + ]) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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, 2) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + assert.equal(skillUsers[0].proficiency, 'Master') + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill1.id) + assert.equal(skillUsers[1].proficiency, 'Beginner') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + + test('allow pivot rows without custom pivot attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Cooking' + + await user.related('skills').saveMany([skill, skill1], true, [ + undefined, + { + proficiency: 'Beginner', + }, + ]) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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, 2) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + assert.isNull(skillUsers[0].proficiency) + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill1.id) + assert.equal(skillUsers[1].proficiency, 'Beginner') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + test('do not attach duplicates when saveMany is called more than once', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true }) @@ -4482,6 +4765,77 @@ test.group('Model | ManyToMany | saveMany', (group) => { assert.isUndefined(skill1.$trx) }) + test('update pivot row when saveMany is called more than once', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = new Skill() + skill.name = 'Programming' + + const skill1 = new Skill() + skill1.name = 'Cooking' + + await user.related('skills').saveMany([skill, skill1], true, [ + { + proficiency: 'Master', + }, + { + proficiency: 'Beginner', + }, + ]) + await user.related('skills').saveMany([skill, skill1], true, [ + { + proficiency: 'Master', + }, + { + proficiency: 'Master', + }, + ]) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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, 2) + + assert.lengthOf(skillUsers, 2) + assert.equal(skillUsers[0].user_id, user.id) + assert.equal(skillUsers[0].skill_id, skill.id) + assert.equal(skillUsers[0].proficiency, 'Master') + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill1.id) + assert.equal(skillUsers[1].proficiency, 'Master') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + test('attach duplicates when saveMany is called more than once with checkExisting = false', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true }) @@ -4721,6 +5075,55 @@ test.group('Model | ManyToMany | create', (group) => { assert.isUndefined(skill.$trx) }) + test('create related instance with pivot attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const skill = await user.related('skills').create( + { name: 'Programming' }, + { + proficiency: 'Master', + } + ) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + + 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.equal(skillUsers[0].proficiency, 'Master') + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + }) + test('wrap create inside a custom transaction', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true }) @@ -4839,6 +5242,118 @@ test.group('Model | ManyToMany | createMany', (group) => { assert.isUndefined(skill1.$trx) }) + test('create many of related instance with pivot attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const [skill, skill1] = await user + .related('skills') + .createMany( + [{ name: 'Programming' }, { name: 'Cooking' }], + [{ proficiency: 'Master' }, { proficiency: 'Beginner' }] + ) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + assert.isTrue(skill1.$isPersisted) + + 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[0].proficiency, 'Master') + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill1.id) + assert.equal(skillUsers[1].proficiency, 'Beginner') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + + test('allow pivot entries without custom attributes', async (assert) => { + class Skill extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public name: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @manyToMany(() => Skill) + public skills: ManyToMany + } + + const user = new User() + user.username = 'virk' + await user.save() + + const [skill, skill1] = await user + .related('skills') + .createMany( + [{ name: 'Programming' }, { name: 'Cooking' }], + [undefined, { proficiency: 'Beginner' }] + ) + + assert.isTrue(user.$isPersisted) + assert.isTrue(skill.$isPersisted) + assert.isTrue(skill1.$isPersisted) + + 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.isNull(skillUsers[0].proficiency) + + assert.equal(skillUsers[1].user_id, user.id) + assert.equal(skillUsers[1].skill_id, skill1.id) + assert.equal(skillUsers[1].proficiency, 'Beginner') + + assert.isUndefined(user.$trx) + assert.isUndefined(skill.$trx) + assert.isUndefined(skill1.$trx) + }) + test('wrap create many inside a custom transaction', async (assert) => { class Skill extends BaseModel { @column({ isPrimary: true })