mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
fix: the mpath is incorrect when the parent of the tree entity is null (#9535)
* fix: the mpath is incorrect when the parent of the tree entity is null * lint: code format * fix: findTrees not have children * test: add unit test * style: format code * fix: unit test * fix: unit test * fix: unit test
This commit is contained in:
parent
97fae631b3
commit
658604d0ae
@ -2,6 +2,9 @@ import { Subject } from "../Subject"
|
||||
import { QueryRunner } from "../../query-runner/QueryRunner"
|
||||
import { OrmUtils } from "../../util/OrmUtils"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
import { EntityMetadata } from "../../metadata/EntityMetadata"
|
||||
import { Brackets } from "../../query-builder/Brackets"
|
||||
|
||||
/**
|
||||
* Executes subject operations for materialized-path tree entities.
|
||||
@ -81,8 +84,14 @@ export class MaterializedPathSubjectExecutor {
|
||||
const oldParent = subject.metadata.treeParentRelation!.getEntityValue(
|
||||
entity!,
|
||||
)
|
||||
const oldParentId = subject.metadata.getEntityIdMap(oldParent)
|
||||
const newParentId = subject.metadata.getEntityIdMap(newParent)
|
||||
const oldParentId = this.getEntityParentReferencedColumnMap(
|
||||
subject,
|
||||
oldParent,
|
||||
)
|
||||
const newParentId = this.getEntityParentReferencedColumnMap(
|
||||
subject,
|
||||
newParent,
|
||||
)
|
||||
|
||||
// Exit if the new and old parents are the same
|
||||
if (OrmUtils.compareIds(oldParentId, newParentId)) {
|
||||
@ -113,7 +122,9 @@ export class MaterializedPathSubjectExecutor {
|
||||
.update(subject.metadata.target)
|
||||
.set({
|
||||
[propertyPath]: () =>
|
||||
`REPLACE(${propertyPath}, '${oldParentPath}${entityPath}.', '${newParentPath}${entityPath}.')`,
|
||||
`REPLACE(${this.queryRunner.connection.driver.escape(
|
||||
propertyPath,
|
||||
)}, '${oldParentPath}${entityPath}.', '${newParentPath}${entityPath}.')`,
|
||||
} as any)
|
||||
.where(`${propertyPath} LIKE :path`, {
|
||||
path: `${oldParentPath}${entityPath}.%`,
|
||||
@ -121,10 +132,30 @@ export class MaterializedPathSubjectExecutor {
|
||||
.execute()
|
||||
}
|
||||
|
||||
private getEntityParentReferencedColumnMap(
|
||||
subject: Subject,
|
||||
entity: ObjectLiteral | undefined,
|
||||
): ObjectLiteral | undefined {
|
||||
if (!entity) return undefined
|
||||
return EntityMetadata.getValueMap(
|
||||
entity,
|
||||
subject.metadata
|
||||
.treeParentRelation!.joinColumns.map(
|
||||
(column) => column.referencedColumn,
|
||||
)
|
||||
.filter((v) => v != null) as ColumnMetadata[],
|
||||
{ skipNulls: true },
|
||||
)
|
||||
}
|
||||
|
||||
private getEntityPath(
|
||||
subject: Subject,
|
||||
id: ObjectLiteral,
|
||||
): Promise<string> {
|
||||
const metadata = subject.metadata
|
||||
const normalized = (Array.isArray(id) ? id : [id]).map((id) =>
|
||||
metadata.ensureEntityIdMap(id),
|
||||
)
|
||||
return this.queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select(
|
||||
@ -134,7 +165,13 @@ export class MaterializedPathSubjectExecutor {
|
||||
"path",
|
||||
)
|
||||
.from(subject.metadata.target, subject.metadata.targetName)
|
||||
.whereInIds(id)
|
||||
.where(
|
||||
new Brackets((qb) => {
|
||||
for (const data of normalized) {
|
||||
qb.orWhere(new Brackets((qb) => qb.where(data)))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.getRawOne()
|
||||
.then((result) => (result ? result["path"] : ""))
|
||||
}
|
||||
|
||||
@ -19,16 +19,20 @@ export class TreeRepositoryUtils {
|
||||
): { id: any; parentId: any }[] {
|
||||
return rawResults.map((rawResult) => {
|
||||
const joinColumn = metadata.treeParentRelation!.joinColumns[0]
|
||||
const referencedColumn =
|
||||
joinColumn.referencedColumn ?? metadata.primaryColumns[0]
|
||||
// fixes issue #2518, default to databaseName property when givenDatabaseName is not set
|
||||
const joinColumnName =
|
||||
joinColumn.givenDatabaseName || joinColumn.databaseName
|
||||
const id =
|
||||
rawResult[alias + "_" + metadata.primaryColumns[0].databaseName]
|
||||
const referencedColumnName =
|
||||
referencedColumn.givenDatabaseName ||
|
||||
referencedColumn.databaseName
|
||||
const id = rawResult[alias + "_" + referencedColumnName]
|
||||
const parentId = rawResult[alias + "_" + joinColumnName]
|
||||
return {
|
||||
id: manager.connection.driver.prepareHydratedValue(
|
||||
id,
|
||||
metadata.primaryColumns[0],
|
||||
referencedColumn,
|
||||
),
|
||||
parentId: manager.connection.driver.prepareHydratedValue(
|
||||
parentId,
|
||||
@ -50,7 +54,10 @@ export class TreeRepositoryUtils {
|
||||
entity[childProperty] = []
|
||||
return
|
||||
}
|
||||
const parentEntityId = metadata.primaryColumns[0].getEntityValue(entity)
|
||||
const joinColumn = metadata.treeParentRelation!.joinColumns[0]
|
||||
const referencedColumn =
|
||||
joinColumn.referencedColumn ?? metadata.primaryColumns[0]
|
||||
const parentEntityId = referencedColumn.getEntityValue(entity)
|
||||
const childRelationMaps = relationMaps.filter(
|
||||
(relationMap) => relationMap.parentId === parentEntityId,
|
||||
)
|
||||
@ -58,7 +65,7 @@ export class TreeRepositoryUtils {
|
||||
childRelationMaps.map((relationMap) => relationMap.id),
|
||||
)
|
||||
entity[childProperty] = entities.filter((entity) =>
|
||||
childIds.has(metadata.primaryColumns[0].getEntityValue(entity)),
|
||||
childIds.has(referencedColumn.getEntityValue(entity)),
|
||||
)
|
||||
entity[childProperty].forEach((childEntity: any) => {
|
||||
TreeRepositoryUtils.buildChildrenEntityTree(
|
||||
@ -81,7 +88,10 @@ export class TreeRepositoryUtils {
|
||||
relationMaps: { id: any; parentId: any }[],
|
||||
): void {
|
||||
const parentProperty = metadata.treeParentRelation!.propertyName
|
||||
const entityId = metadata.primaryColumns[0].getEntityValue(entity)
|
||||
const joinColumn = metadata.treeParentRelation!.joinColumns[0]
|
||||
const referencedColumn =
|
||||
joinColumn.referencedColumn ?? metadata.primaryColumns[0]
|
||||
const entityId = referencedColumn.getEntityValue(entity)
|
||||
const parentRelationMap = relationMaps.find(
|
||||
(relationMap) => relationMap.id === entityId,
|
||||
)
|
||||
@ -89,7 +99,7 @@ export class TreeRepositoryUtils {
|
||||
if (!parentRelationMap) return false
|
||||
|
||||
return (
|
||||
metadata.primaryColumns[0].getEntityValue(entity) ===
|
||||
referencedColumn.getEntityValue(entity) ===
|
||||
parentRelationMap.parentId
|
||||
)
|
||||
})
|
||||
|
||||
41
test/github-issues/9534/entity/Category.ts
Normal file
41
test/github-issues/9534/entity/Category.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"
|
||||
import { Column } from "../../../../src/decorator/columns/Column"
|
||||
import { TreeParent } from "../../../../src/decorator/tree/TreeParent"
|
||||
import { TreeChildren } from "../../../../src/decorator/tree/TreeChildren"
|
||||
import { Entity } from "../../../../src/decorator/entity/Entity"
|
||||
import { Tree } from "../../../../src/decorator/tree/Tree"
|
||||
import { JoinColumn } from "../../../../src/decorator/relations/JoinColumn"
|
||||
|
||||
@Entity({ name: "categories" })
|
||||
@Tree("materialized-path")
|
||||
export class Category {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
type: "varchar",
|
||||
name: "uid",
|
||||
unique: true,
|
||||
})
|
||||
uid: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column({
|
||||
type: "varchar",
|
||||
name: "parentUid",
|
||||
nullable: true,
|
||||
})
|
||||
parentUid?: string | null
|
||||
|
||||
@TreeParent()
|
||||
@JoinColumn({
|
||||
name: "parentUid",
|
||||
referencedColumnName: "uid",
|
||||
})
|
||||
parentCategory?: Category | null
|
||||
|
||||
@TreeChildren({ cascade: true })
|
||||
childCategories: Category[]
|
||||
}
|
||||
243
test/github-issues/9534/issue-9534.ts
Normal file
243
test/github-issues/9534/issue-9534.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import "reflect-metadata"
|
||||
import { Category } from "./entity/Category"
|
||||
import { DataSource } from "../../../src/data-source/DataSource"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../utils/test-utils"
|
||||
|
||||
describe("github issues > #9534 materialized-path", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [Category],
|
||||
// logging: true,
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("attach should work properly", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const categoryRepository =
|
||||
connection.getTreeRepository(Category)
|
||||
|
||||
const a1 = new Category()
|
||||
a1.name = "a1"
|
||||
a1.uid = "a1"
|
||||
await categoryRepository.save(a1)
|
||||
|
||||
const a11 = new Category()
|
||||
a11.name = "a11"
|
||||
a11.uid = "a11"
|
||||
a11.parentCategory = a1
|
||||
await categoryRepository.save(a11)
|
||||
|
||||
const a111 = new Category()
|
||||
a111.name = "a111"
|
||||
a111.uid = "a111"
|
||||
a111.parentCategory = a11
|
||||
await categoryRepository.save(a111)
|
||||
|
||||
const a12 = new Category()
|
||||
a12.name = "a12"
|
||||
a12.uid = "a12"
|
||||
a12.parentCategory = a1
|
||||
await categoryRepository.save(a12)
|
||||
// normal part
|
||||
{
|
||||
const rootCategories = await categoryRepository.findRoots()
|
||||
rootCategories.should.be.eql([
|
||||
{
|
||||
id: 1,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
},
|
||||
])
|
||||
|
||||
const a11Parent = await categoryRepository.findAncestors(
|
||||
a11,
|
||||
)
|
||||
a11Parent.length.should.be.equal(2)
|
||||
a11Parent.should.deep.include({
|
||||
id: 1,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
})
|
||||
a11Parent.should.deep.include({
|
||||
id: 2,
|
||||
name: "a11",
|
||||
uid: "a11",
|
||||
parentUid: "a1",
|
||||
})
|
||||
|
||||
const a1Children = await categoryRepository.findDescendants(
|
||||
a1,
|
||||
)
|
||||
a1Children.length.should.be.equal(4)
|
||||
a1Children.should.deep.include({
|
||||
id: 1,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
})
|
||||
a1Children.should.deep.include({
|
||||
id: 2,
|
||||
name: "a11",
|
||||
uid: "a11",
|
||||
parentUid: "a1",
|
||||
})
|
||||
a1Children.should.deep.include({
|
||||
id: 3,
|
||||
name: "a111",
|
||||
uid: "a111",
|
||||
parentUid: "a11",
|
||||
})
|
||||
a1Children.should.deep.include({
|
||||
id: 4,
|
||||
name: "a12",
|
||||
uid: "a12",
|
||||
parentUid: "a1",
|
||||
})
|
||||
}
|
||||
|
||||
a111.parentCategory = null
|
||||
await categoryRepository.save(a111)
|
||||
|
||||
const rootCategories = await categoryRepository.findRoots()
|
||||
rootCategories.should.be.eql([
|
||||
{
|
||||
id: 1,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "a111",
|
||||
uid: "a111",
|
||||
parentUid: null,
|
||||
},
|
||||
])
|
||||
|
||||
const a11Parent = await categoryRepository.findAncestors(a11)
|
||||
a11Parent.length.should.be.equal(2)
|
||||
a11Parent.should.deep.include({
|
||||
id: 1,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
})
|
||||
a11Parent.should.deep.include({
|
||||
id: 2,
|
||||
name: "a11",
|
||||
uid: "a11",
|
||||
parentUid: "a1",
|
||||
})
|
||||
|
||||
const a1Children = await categoryRepository.findDescendants(a1)
|
||||
a1Children.length.should.be.equal(3)
|
||||
a1Children.should.deep.include({
|
||||
id: 1,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
})
|
||||
a1Children.should.deep.include({
|
||||
id: 2,
|
||||
name: "a11",
|
||||
uid: "a11",
|
||||
parentUid: "a1",
|
||||
})
|
||||
a1Children.should.deep.include({
|
||||
id: 4,
|
||||
name: "a12",
|
||||
uid: "a12",
|
||||
parentUid: "a1",
|
||||
})
|
||||
}),
|
||||
))
|
||||
it("findTrees() tests > findTrees should load all category roots", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const categoryRepository =
|
||||
connection.getTreeRepository(Category)
|
||||
|
||||
const a1 = new Category()
|
||||
a1.name = "a1"
|
||||
a1.uid = "a1"
|
||||
|
||||
const a11 = new Category()
|
||||
a11.name = "a11"
|
||||
a11.uid = "a11"
|
||||
|
||||
const a12 = new Category()
|
||||
a12.name = "a12"
|
||||
a12.uid = "a12"
|
||||
|
||||
const a111 = new Category()
|
||||
a111.name = "a111"
|
||||
a111.uid = "a111"
|
||||
|
||||
const a112 = new Category()
|
||||
a112.name = "a112"
|
||||
a112.uid = "a112"
|
||||
|
||||
a1.childCategories = [a11, a12]
|
||||
a11.childCategories = [a111, a112]
|
||||
await categoryRepository.save(a1)
|
||||
|
||||
const categoriesTree = await categoryRepository.findTrees()
|
||||
// using sort because some drivers returns arrays in wrong order
|
||||
categoriesTree[0].childCategories.sort((a, b) => a.id - b.id)
|
||||
categoriesTree[0].childCategories[0].childCategories.sort(
|
||||
(a, b) => a.id - b.id,
|
||||
)
|
||||
|
||||
categoriesTree.should.be.eql([
|
||||
{
|
||||
id: a1.id,
|
||||
name: "a1",
|
||||
uid: "a1",
|
||||
parentUid: null,
|
||||
childCategories: [
|
||||
{
|
||||
id: a11.id,
|
||||
name: "a11",
|
||||
uid: "a11",
|
||||
parentUid: "a1",
|
||||
childCategories: [
|
||||
{
|
||||
id: a111.id,
|
||||
name: "a111",
|
||||
uid: "a111",
|
||||
parentUid: "a11",
|
||||
childCategories: [],
|
||||
},
|
||||
{
|
||||
id: a112.id,
|
||||
name: "a112",
|
||||
uid: "a112",
|
||||
parentUid: "a11",
|
||||
childCategories: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: a12.id,
|
||||
name: "a12",
|
||||
uid: "a12",
|
||||
parentUid: "a1",
|
||||
childCategories: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
}),
|
||||
))
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user