Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: OneToManySubjectBuilder bug with multiple primary keys (#8221)
* Bugfix: OneToManySubjectBuilder generated invalid subjects because of failed matching of relation IDs. * relation.getEntityValue does not always return an array. Fix by defaulting to empty array on falsy return value. * Add tests * test fixes * Refactor tests ensuring composite keys on child side into a separate test suite @ functional tests * Rewrite tests and notes to correctly document+show what's the actual issue * Fix: test must not use Promise.all, parallel execution against different drivers would mess up the counter within the SettingSubscriber! * code updates * okay now I know we need this check Co-authored-by: Jannik <jannik@jannikmewes.de> Co-authored-by: jannik.wjm@gmail.com <> Co-authored-by: Umed Khudoiberdiev <pleerock.me@gmail.com>
- Loading branch information
1 parent
28c183e
commit 6558295
Showing
8 changed files
with
326 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
...ional/relations/multiple-primary-keys/multiple-primary-keys-one-to-many/entity/Setting.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryColumn } from "../../../../../../src"; | ||
import {User} from "./User"; | ||
|
||
@Entity() | ||
export class Setting extends BaseEntity { | ||
@PrimaryColumn("int") | ||
assetId?: number; | ||
|
||
@ManyToOne("User","settings",{ cascade:false , orphanedRowAction: "delete", nullable:false }) | ||
asset?: User; | ||
|
||
@PrimaryColumn("varchar") | ||
name: string; | ||
|
||
@Column({nullable:true}) | ||
value: string; | ||
|
||
constructor(id: number, name: string, value: string) { | ||
super(); | ||
this.assetId = id; | ||
this.name = name; | ||
this.value = value; | ||
} | ||
|
||
} |
20 changes: 20 additions & 0 deletions
20
...nctional/relations/multiple-primary-keys/multiple-primary-keys-one-to-many/entity/User.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { BaseEntity, Column, Entity, OneToMany, PrimaryColumn } from "../../../../../../src"; | ||
import {Setting} from "./Setting"; | ||
|
||
@Entity() | ||
export class User extends BaseEntity { | ||
@PrimaryColumn() | ||
id: number; | ||
|
||
@Column() | ||
name: string; | ||
|
||
@OneToMany("Setting","asset",{ cascade:true }) | ||
settings: Setting[]; | ||
|
||
constructor(id: number, name: string) { | ||
super(); | ||
this.id = id; | ||
this.name = name; | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
...tiple-primary-keys/multiple-primary-keys-one-to-many/multiple-primary-keys-one-to-many.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { expect } from "chai"; | ||
import "reflect-metadata"; | ||
import { Connection } from "../../../../../src"; | ||
import { closeTestingConnections, createTestingConnections } from "../../../../utils/test-utils"; | ||
import { User } from "./entity/User"; | ||
import { Setting } from "./entity/Setting"; | ||
|
||
/** | ||
* Using OneToMany relation with composed primary key should not error and work correctly | ||
*/ | ||
describe("relations > multiple-primary-keys > one-to-many", () => { | ||
|
||
let connections: Connection[]; | ||
|
||
before(async () => connections = await createTestingConnections({ | ||
entities: [User,Setting], | ||
schemaCreate: true, | ||
dropSchema: true | ||
})); | ||
|
||
after(() => closeTestingConnections(connections)); | ||
|
||
function insertSimpleTestData(connection: Connection) { | ||
const userRepo = connection.getRepository(User); | ||
// const settingRepo = connection.getRepository(Setting); | ||
|
||
const user = new User(1, "FooGuy"); | ||
const settingA = new Setting(1, "A", "foo"); | ||
const settingB = new Setting(1, "B", "bar"); | ||
user.settings = [settingA,settingB]; | ||
|
||
return userRepo.save(user); | ||
} | ||
|
||
|
||
|
||
it("should correctly insert relation items", () => Promise.all(connections.map(async connection => { | ||
|
||
const userEntity = await insertSimpleTestData(connection); | ||
const persistedSettings = await connection.getRepository(Setting).find(); | ||
|
||
expect(persistedSettings!).not.to.be.undefined; | ||
expect(persistedSettings.length).to.equal(2); | ||
expect(persistedSettings[0].assetId).to.equal(userEntity.id); | ||
expect(persistedSettings[1].assetId).to.equal(userEntity.id); | ||
|
||
}))); | ||
|
||
it("should correctly load relation items", () => Promise.all(connections.map(async connection => { | ||
|
||
await insertSimpleTestData(connection); | ||
const user = await connection.getRepository(User).findOne({relations:["settings"]}); | ||
|
||
expect(user!).not.to.be.undefined; | ||
expect(user!.settings).to.be.an("array"); | ||
expect(user!.settings!.length).to.equal(2); | ||
|
||
}))); | ||
|
||
it("should correctly update relation items", () => Promise.all(connections.map(async connection => { | ||
|
||
await insertSimpleTestData(connection); | ||
const userRepo = connection.getRepository(User); | ||
|
||
await userRepo.save([{ | ||
id:1, | ||
settings:[ | ||
{id:1,name:"A",value:"foobar"}, | ||
{id:1,name:"B",value:"testvalue"}, | ||
] | ||
}]); | ||
|
||
const user = await connection.getRepository(User).findOne({relations:["settings"]}); | ||
|
||
// check the saved items have correctly updated value | ||
expect(user!).not.to.be.undefined; | ||
expect(user!.settings).to.be.an("array"); | ||
expect(user!.settings!.length).to.equal(2); | ||
user!.settings.forEach(setting=>{ | ||
if(setting.name==="A") expect(setting.value).to.equal("foobar"); | ||
else expect(setting.value).to.equal("testvalue"); | ||
}); | ||
|
||
// make sure only 2 entries are in db, initial ones should have been updated | ||
const settings = await connection.getRepository(Setting).find(); | ||
expect(settings).to.be.an("array"); | ||
expect(settings!.length).to.equal(2); | ||
|
||
}))); | ||
|
||
it("should correctly delete relation items", () => Promise.all(connections.map(async connection => { | ||
|
||
await insertSimpleTestData(connection); | ||
const userRepo = connection.getRepository(User); | ||
|
||
await userRepo.save([{ | ||
id:1, | ||
settings:[] | ||
}]); | ||
|
||
const user = await connection.getRepository(User).findOne({relations:["settings"]}); | ||
|
||
// check that no relational items are found | ||
expect(user!).not.to.be.undefined; | ||
expect(user!.settings).to.be.an("array"); | ||
expect(user!.settings!.length).to.equal(0); | ||
|
||
// check there are no orphane relational items | ||
const settings = await connection.getRepository(Setting).find(); | ||
expect(settings).to.be.an("array"); | ||
expect(settings!.length).to.equal(0); | ||
|
||
}))); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryColumn } from "../../../../src"; | ||
import {User} from "./User"; | ||
|
||
@Entity() | ||
export class Setting extends BaseEntity { | ||
@PrimaryColumn("int") | ||
assetId?: number; | ||
|
||
@ManyToOne("User","settings",{ cascade:false , orphanedRowAction: "delete", nullable:false }) | ||
asset?: User; | ||
|
||
@PrimaryColumn("varchar") | ||
name: string; | ||
|
||
@Column({nullable:true}) | ||
value: string; | ||
|
||
constructor(id: number, name: string, value: string) { | ||
super(); | ||
this.assetId = id; | ||
this.name = name; | ||
this.value = value; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { EntitySubscriberInterface, EventSubscriber, LoadEvent, UpdateEvent } from "../../../../src"; | ||
import {Setting} from "./Setting"; | ||
|
||
|
||
@EventSubscriber() | ||
export class SettingSubscriber implements EntitySubscriberInterface { | ||
counter: any; | ||
|
||
constructor() { | ||
this.reset(); | ||
} | ||
|
||
listenTo() { | ||
return Setting; | ||
} | ||
|
||
afterLoad(item: Setting, event?: LoadEvent<Setting>) { | ||
// just an example, any entity modification on after load will lead to this issue | ||
item.value = "x"; | ||
} | ||
|
||
beforeUpdate(event: UpdateEvent<any>): void { | ||
this.counter.updates++; | ||
} | ||
|
||
beforeInsert(event: UpdateEvent<any>): void { | ||
this.counter.inserts++; | ||
} | ||
|
||
beforeRemove(event: UpdateEvent<any>): void { | ||
this.counter.deletes++; | ||
} | ||
|
||
reset() { | ||
this.counter = { | ||
deletes:0, | ||
inserts:0, | ||
updates:0, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { BaseEntity, Column, Entity, OneToMany, PrimaryColumn } from "../../../../src"; | ||
import {Setting} from "./Setting"; | ||
|
||
@Entity() | ||
export class User extends BaseEntity { | ||
@PrimaryColumn() | ||
id: number; | ||
|
||
@Column() | ||
name: string; | ||
|
||
@OneToMany("Setting","asset",{ cascade:true }) | ||
settings: Setting[]; | ||
|
||
constructor(id: number, name: string) { | ||
super(); | ||
this.id = id; | ||
this.name = name; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { expect } from "chai"; | ||
import "reflect-metadata"; | ||
import { Connection } from "../../../src"; | ||
import { closeTestingConnections, createTestingConnections } from "../../utils/test-utils"; | ||
import { User } from "./entity/User"; | ||
import { Setting } from "./entity/Setting"; | ||
import { SettingSubscriber } from "./entity/SettingSubscriber"; | ||
|
||
/** | ||
* Using OneToMany relation with composed primary key should not error and work correctly | ||
*/ | ||
describe("github issues > #8221", () => { | ||
|
||
let connections: Connection[]; | ||
|
||
before(async () => connections = await createTestingConnections({ | ||
entities: [User,Setting], | ||
subscribers: [SettingSubscriber], | ||
schemaCreate: true, | ||
dropSchema: true | ||
})); | ||
|
||
after(() => closeTestingConnections(connections)); | ||
|
||
function insertSimpleTestData(connection: Connection) { | ||
const userRepo = connection.getRepository(User); | ||
// const settingRepo = connection.getRepository(Setting); | ||
|
||
const user = new User(1, "FooGuy"); | ||
const settingA = new Setting(1, "A", "foo"); | ||
const settingB = new Setting(1, "B", "bar"); | ||
user.settings = [settingA,settingB]; | ||
|
||
return userRepo.save(user); | ||
} | ||
|
||
// important: must not use Promise.all! parallel execution against different drivers would mess up the counter within the SettingSubscriber! | ||
|
||
it("afterLoad entity modifier must not make relation key matching fail", async () => { | ||
for(const connection of connections) { | ||
|
||
const userRepo = connection.getRepository(User); | ||
const subscriber = (connection.subscribers[0] as SettingSubscriber); | ||
subscriber.reset(); | ||
|
||
await insertSimpleTestData(connection); | ||
subscriber.reset(); | ||
|
||
await userRepo.save([{ | ||
id:1, | ||
settings: [ | ||
{ assertId:1, name:"A", value:"foobar" }, | ||
{ assertId:1, name:"B", value:"testvalue" }, | ||
] | ||
}]); | ||
|
||
// we use a subscriber to count generated Subjects based on how often beforeInsert/beforeRemove/beforeUpdate has been called. | ||
// the save query should only update settings, so only beforeUpdate should have been called. | ||
// if beforeInsert/beforeUpdate has been called, this would indicate that key matching has failed. | ||
// the resulting state would be the same, but settings entities would be deleted and inserted instead. | ||
|
||
expect(subscriber.counter.deletes).to.equal(0); | ||
expect(subscriber.counter.inserts).to.equal(0); | ||
expect(subscriber.counter.updates).to.equal(2); | ||
|
||
} | ||
}); | ||
|
||
}); |