Skip to content

Commit

Permalink
fix: create correct children during cascade saving entities with STI (#…
Browse files Browse the repository at this point in the history
…9034)

* test: test saving disciminators STI, cascading

This commit adds an test for checking whether discriminators are saved
correctly when saving a field with cascade that uses
Single-Table-Inheritance.

Related to: #7758

* fix: Create correct children with STI

This commit fixes the `create` function for EntityManager and Repository
to create entities of correct type when using Single Table Inheritance.
Refactors the otherwise repeated code into a new function on
EntityMetadata.

Related to: #7758

* test: check STI type setting discriminator manually

Related to: #9033

* feature: allow setting discriminator value manually

This commit allows using an instance of a base class in a
Single Table Inheritance scenario and setting the discriminator value
manually.

Related to: #9033

* test: test saving disciminators with trees in STI

This commit adds an test for checking whether discriminators are saved
correctly when saving a tree that also uses Single-Table-Inheritance.

Related to: #7758

* fix: Create correct children with STI and trees

This commit fixes the `create` function for EntityManager and TreeRepository
to create entities of correct type when using Single Table Inheritance
and complex inheritance with Trees.

Related to: #7758
  • Loading branch information
felix-gohla committed May 9, 2023
1 parent 96b7ee4 commit 06c1e98
Show file tree
Hide file tree
Showing 18 changed files with 507 additions and 39 deletions.
17 changes: 17 additions & 0 deletions src/metadata-builder/EntityMetadataBuilder.ts
Expand Up @@ -607,6 +607,23 @@ export class EntityMetadataBuilder {
) {
entityMetadata.ownColumns.push(discriminatorColumn)
}
// also copy the inheritance pattern & tree metadata
// this comes in handy when inheritance and trees are used together
entityMetadata.inheritancePattern =
entityMetadata.parentEntityMetadata.inheritancePattern
if (
!entityMetadata.treeType &&
!!entityMetadata.parentEntityMetadata.treeType
) {
entityMetadata.treeType =
entityMetadata.parentEntityMetadata.treeType
entityMetadata.treeOptions =
entityMetadata.parentEntityMetadata.treeOptions
entityMetadata.treeParentRelation =
entityMetadata.parentEntityMetadata.treeParentRelation
entityMetadata.treeLevelColumn =
entityMetadata.parentEntityMetadata.treeLevelColumn
}
}

const { namingStrategy } = this.connection
Expand Down
56 changes: 46 additions & 10 deletions src/metadata/EntityMetadata.ts
Expand Up @@ -829,31 +829,67 @@ export class EntityMetadata {
relationsAndValues.push([
relation,
subValue,
this.getInverseEntityMetadata(subValue, relation),
EntityMetadata.getInverseEntityMetadata(
subValue,
relation,
),
]),
)
} else if (value) {
relationsAndValues.push([
relation,
value,
this.getInverseEntityMetadata(value, relation),
EntityMetadata.getInverseEntityMetadata(value, relation),
])
}
})
return relationsAndValues
}

private getInverseEntityMetadata(
/**
* In the case of SingleTableInheritance, find the correct metadata
* for a given value.
*
* @param value The value to find the metadata for.
* @returns The found metadata for the entity or the base metadata if no matching metadata
* was found in the whole inheritance tree.
*/
findInheritanceMetadata(value: any): EntityMetadata {
// Check for single table inheritance and find the correct metadata in that case.
// Goal is to use the correct discriminator as we could have a repository
// for an (abstract) base class and thus the target would not match.

if (
this.inheritancePattern === "STI" &&
this.childEntityMetadatas.length > 0
) {
// There could be a column on the base class that can manually be set to override the type.
let manuallySetDiscriminatorValue: unknown
if (this.discriminatorColumn) {
manuallySetDiscriminatorValue =
value[this.discriminatorColumn.propertyName]
}
return (
this.childEntityMetadatas.find(
(meta) =>
manuallySetDiscriminatorValue ===
meta.discriminatorValue ||
value.constructor === meta.target,
) || this
)
}
return this
}

// -------------------------------------------------------------------------
// Private Static Methods
// -------------------------------------------------------------------------

private static getInverseEntityMetadata(
value: any,
relation: RelationMetadata,
): EntityMetadata {
const childEntityMetadata =
relation.inverseEntityMetadata.childEntityMetadatas.find(
(metadata) => metadata.target === value.constructor,
)
return childEntityMetadata
? childEntityMetadata
: relation.inverseEntityMetadata
return relation.inverseEntityMetadata.findInheritanceMetadata(value)
}

// -------------------------------------------------------------------------
Expand Down
21 changes: 3 additions & 18 deletions src/persistence/EntityPersistExecutor.ts
Expand Up @@ -81,24 +81,9 @@ export class EntityPersistExecutor {
if (entityTarget === Object)
throw new CannotDetermineEntityError(this.mode)

let metadata = this.connection.getMetadata(entityTarget)

// Check for single table inheritance and find the correct metadata in that case.
// Goal is to use the correct discriminator as we could have a repository
// for an (abstract) base class and thus the target would not match.
if (
metadata.inheritancePattern === "STI" &&
metadata.childEntityMetadatas.length > 0
) {
const matchingChildMetadata =
metadata.childEntityMetadatas.find(
(meta) =>
entity.constructor === meta.target,
)
if (matchingChildMetadata) {
metadata = matchingChildMetadata
}
}
let metadata = this.connection
.getMetadata(entityTarget)
.findInheritanceMetadata(entity)

subjects.push(
new Subject({
Expand Down
29 changes: 20 additions & 9 deletions src/query-builder/transformer/PlainObjectToNewEntityTransformer.ts
Expand Up @@ -82,20 +82,24 @@ export class PlainObjectToNewEntityTransformer {
)
})

const inverseEntityMetadata =
relation.inverseEntityMetadata.findInheritanceMetadata(
objectRelatedValueItem,
)

// if such item already exist then merge new data into it, if its not we create a new entity and merge it into the array
if (!objectRelatedValueEntity) {
objectRelatedValueEntity =
relation.inverseEntityMetadata.create(
undefined,
{ fromDeserializer: true },
)
inverseEntityMetadata.create(undefined, {
fromDeserializer: true,
})
entityRelatedValue.push(objectRelatedValueEntity)
}

this.groupAndTransform(
objectRelatedValueEntity,
objectRelatedValueItem,
relation.inverseEntityMetadata,
inverseEntityMetadata,
getLazyRelationsPromiseValue,
)
})
Expand All @@ -110,18 +114,25 @@ export class PlainObjectToNewEntityTransformer {
return
}

const inverseEntityMetadata =
relation.inverseEntityMetadata.findInheritanceMetadata(
objectRelatedValue,
)

if (!entityRelatedValue) {
entityRelatedValue =
relation.inverseEntityMetadata.create(undefined, {
entityRelatedValue = inverseEntityMetadata.create(
undefined,
{
fromDeserializer: true,
})
},
)
relation.setEntityValue(entity, entityRelatedValue)
}

this.groupAndTransform(
entityRelatedValue,
objectRelatedValue,
relation.inverseEntityMetadata,
inverseEntityMetadata,
getLazyRelationsPromiseValue,
)
}
Expand Down
4 changes: 2 additions & 2 deletions test/github-issues/2927/issue-2927.ts
Expand Up @@ -41,7 +41,7 @@ describe("github issues > #2927 When using base class' custom repository, the di
// Retrieve it back from the DB.
const contents = await repository.find()
expect(contents.length).to.equal(1)
expect(contents[0] instanceof Photo).to.equal(true)
expect(contents[0]).to.be.an.instanceOf(Photo)
const fetchedPhoto = contents[0] as Photo
expect(fetchedPhoto).to.eql(photo)
}),
Expand All @@ -64,7 +64,7 @@ describe("github issues > #2927 When using base class' custom repository, the di
// Retrieve it back from the DB.
const contents = await repository.find()
expect(contents.length).to.equal(1)
expect(contents[0] instanceof SpecialPhoto).to.equal(true)
expect(contents[0]).to.be.an.instanceOf(SpecialPhoto)
const fetchedSpecialPhoto = contents[0] as SpecialPhoto
expect(fetchedSpecialPhoto).to.eql(specialPhoto)
}),
Expand Down
25 changes: 25 additions & 0 deletions test/github-issues/7558/entity/Animal.ts
@@ -0,0 +1,25 @@
import {
Column,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
TableInheritance,
} from "../../../../src"

import { PersonEntity } from "./Person"

@Entity("animal")
@TableInheritance({ column: { type: "varchar", name: "type" } })
export class AnimalEntity {
@PrimaryGeneratedColumn()
id: number

@Column({ type: "varchar" })
name: string

@ManyToOne(() => PersonEntity, ({ pets }) => pets, {
onDelete: "CASCADE",
onUpdate: "CASCADE",
})
person: PersonEntity
}
10 changes: 10 additions & 0 deletions test/github-issues/7558/entity/Cat.ts
@@ -0,0 +1,10 @@
import { ChildEntity, Column } from "../../../../src"

import { AnimalEntity } from "./Animal"

@ChildEntity("cat")
export class CatEntity extends AnimalEntity {
// Cat stuff
@Column()
livesLeft: number
}
16 changes: 16 additions & 0 deletions test/github-issues/7558/entity/Content.ts
@@ -0,0 +1,16 @@
import {
Column,
Entity,
PrimaryGeneratedColumn,
TableInheritance,
} from "../../../../src"

@Entity("content")
@TableInheritance({ column: { type: "varchar", name: "type" } })
export class Content {
@PrimaryGeneratedColumn()
id: number

@Column()
title: string
}
10 changes: 10 additions & 0 deletions test/github-issues/7558/entity/Dog.ts
@@ -0,0 +1,10 @@
import { ChildEntity, Column } from "../../../../src"

import { AnimalEntity } from "./Animal"

@ChildEntity("dog")
export class DogEntity extends AnimalEntity {
// Dog stuff
@Column()
steaksEaten: number
}
12 changes: 12 additions & 0 deletions test/github-issues/7558/entity/NnaryOperator.ts
@@ -0,0 +1,12 @@
import { ChildEntity, Column, TreeChildren } from "../../../../src"

import { OperatorTreeEntry } from "./OperatorTreeEntry"

@ChildEntity("nnary")
export class NnaryOperator extends OperatorTreeEntry {
@TreeChildren({ cascade: true })
children: OperatorTreeEntry[]

@Column()
operator: string
}
11 changes: 11 additions & 0 deletions test/github-issues/7558/entity/NumberEntry.ts
@@ -0,0 +1,11 @@
import { ChildEntity, Column } from "../../../../src"

import { OperatorTreeEntry } from "./OperatorTreeEntry"

@ChildEntity("number")
export class NumberEntry extends OperatorTreeEntry {
@Column({
type: "float",
})
value: number
}
19 changes: 19 additions & 0 deletions test/github-issues/7558/entity/OperatorTreeEntry.ts
@@ -0,0 +1,19 @@
import {
Entity,
PrimaryGeneratedColumn,
TableInheritance,
Tree,
TreeParent,
} from "../../../../src"
import type { NnaryOperator } from "./NnaryOperator"

@Entity()
@TableInheritance({ pattern: "STI", column: { type: "varchar" } })
@Tree("closure-table")
export class OperatorTreeEntry {
@PrimaryGeneratedColumn("uuid")
id: string

@TreeParent()
parent?: NnaryOperator
}
30 changes: 30 additions & 0 deletions test/github-issues/7558/entity/Person.ts
@@ -0,0 +1,30 @@
import {
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from "../../../../src"

import { AnimalEntity } from "./Animal"
import { Content } from "./Content"

@Entity({ name: "person" })
export class PersonEntity {
@PrimaryGeneratedColumn()
id!: number

@OneToMany(() => AnimalEntity, ({ person }) => person, {
cascade: true,
eager: true,
})
pets!: AnimalEntity[]

@OneToOne(() => Content, {
cascade: true,
eager: true,
nullable: true,
})
@JoinColumn()
content?: Content
}
8 changes: 8 additions & 0 deletions test/github-issues/7558/entity/Photo.ts
@@ -0,0 +1,8 @@
import { ChildEntity, Column } from "../../../../src"
import { Content } from "./Content"

@ChildEntity("photo")
export class Photo extends Content {
@Column()
size: number
}

0 comments on commit 06c1e98

Please sign in to comment.