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:
Sakura 2022-12-03 20:21:40 +08:00 committed by GitHub
parent 97fae631b3
commit 658604d0ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 342 additions and 11 deletions

View File

@ -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"] : ""))
}

View File

@ -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
)
})

View 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[]
}

View 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: [],
},
],
},
])
}),
))
})