Skip to content

Commit

Permalink
feat: add tree entities update and delete logic (#7156)
Browse files Browse the repository at this point in the history
* feat: add tree entities update and delete logic

Allows tree entities relations to be updated and deleted via the save function from the Repository

Closes: #7155

* fix: fix linting errors

* fix: revert development changes

* fix: remove commented code

* fix: remove LIMIT 1 to fix Oracle test

* fix: fix mssql onDelete CASCADE for the closure juntion table

* fix: add a await insert to the junction table query

* feat: add closure junction cascade for all drives except mssql

Mssql uses a conventional delete to manage the junction table
Allow TreeParent onDelete CASCANDE on all drivers except mssql,
mssql will throw an error saying that the feature is not supported.

* fix: add try catch to getMetadata on createForeignKey in SqlServerQueryRunner

* fix: fix entities path typo

* fix: fix issue regarding relation in for tree entities

* fix: make tree relation tests run in all drivers

* fix: fix tests by setting relation tree entities in a new file

* fix: enable re-adding a parent to an entity with a previous null parent

* refactor: replace a try catch with a ternary operator
  • Loading branch information
mateussilva92 committed May 18, 2021
1 parent 44979af commit 9c8a3fb
Show file tree
Hide file tree
Showing 17 changed files with 3,104 additions and 67 deletions.
2 changes: 1 addition & 1 deletion src/decorator/relations/ManyToOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function ManyToOne<T>(typeFunctionOrTarget: string|((type?: any) => Objec
if (!options) options = {} as RelationOptions;

// Now try to determine if it is a lazy relation.
let isLazy = options && options.lazy === true ? true : false;
let isLazy = options && options.lazy === true;
if (!isLazy && Reflect && (Reflect as any).getMetadata) { // automatic determination
const reflectedType = (Reflect as any).getMetadata("design:type", object, propertyName);
if (reflectedType && typeof reflectedType.name === "string" && reflectedType.name.toLowerCase() === "promise")
Expand Down
2 changes: 1 addition & 1 deletion src/decorator/relations/OneToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function OneToMany<T>(typeFunctionOrTarget: string|((type?: any) => Objec
if (!options) options = {} as RelationOptions;

// Now try to determine if it is a lazy relation.
let isLazy = options && options.lazy === true ? true : false;
let isLazy = options && options.lazy === true;
if (!isLazy && Reflect && (Reflect as any).getMetadata) { // automatic determination
const reflectedType = (Reflect as any).getMetadata("design:type", object, propertyName);
if (reflectedType && typeof reflectedType.name === "string" && reflectedType.name.toLowerCase() === "promise")
Expand Down
7 changes: 5 additions & 2 deletions src/decorator/tree/TreeParent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {getMetadataArgsStorage} from "../../";
import {RelationMetadataArgs} from "../../metadata-args/RelationMetadataArgs";
import {OnDeleteType} from "../../metadata/types/OnDeleteType";
import {RelationOptions} from "../options/RelationOptions";

/**
* Marks a entity property as a parent of the tree.
* "Tree parent" indicates who owns (is a parent) of this entity in tree structure.
*/
export function TreeParent(): PropertyDecorator {
export function TreeParent(options?: { onDelete?: OnDeleteType }): PropertyDecorator {
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;

// now try to determine it its lazy relation
const reflectedType = Reflect && (Reflect as any).getMetadata ? Reflect.getMetadata("design:type", object, propertyName) : undefined;
Expand All @@ -19,7 +22,7 @@ export function TreeParent(): PropertyDecorator {
isLazy: isLazy,
relationType: "many-to-one",
type: () => object.constructor,
options: {}
options: options
} as RelationMetadataArgs);
};
}
4 changes: 4 additions & 0 deletions src/driver/sqlserver/SqlServerQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,10 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner
*/
async createForeignKey(tableOrName: Table|string, foreignKey: TableForeignKey): Promise<void> {
const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName);
const metadata = this.connection.hasMetadata(table.name) ? this.connection.getMetadata(table.name) : undefined;

if (metadata && metadata.treeParentRelation && metadata.treeParentRelation!.isTreeParent && metadata.foreignKeys.find(foreignKey => foreignKey.onDelete !== "NO ACTION"))
throw new Error("SqlServer does not support options in TreeParent.");

// new FK may be passed without name. In this case we generate FK name manually.
if (!foreignKey.name)
Expand Down
10 changes: 10 additions & 0 deletions src/error/NestedSetMultipleRootError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class NestedSetMultipleRootError extends Error {
name = "NestedSetMultipleRootError";

constructor() {
super();
Object.setPrototypeOf(this, NestedSetMultipleRootError.prototype);
this.message = `Nested sets do not support multiple root entities.`;
}

}
6 changes: 4 additions & 2 deletions src/metadata-builder/ClosureJunctionEntityMetadataBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
import {Connection} from "../connection/Connection";
import {IndexMetadata} from "../metadata/IndexMetadata";
import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";

/**
* Creates EntityMetadata for junction tables of the closure entities.
Expand Down Expand Up @@ -110,20 +111,21 @@ export class ClosureJunctionEntityMetadataBuilder {
}

// create junction table foreign keys
// Note: CASCADE is not applied to mssql because it does not support multi cascade paths
entityMetadata.foreignKeys = [
new ForeignKeyMetadata({
entityMetadata: entityMetadata,
referencedEntityMetadata: parentClosureEntityMetadata,
columns: [entityMetadata.ownColumns[0]],
referencedColumns: parentClosureEntityMetadata.primaryColumns,
// onDelete: "CASCADE" // todo: does not work in mssql for some reason
onDelete: this.connection.driver instanceof SqlServerDriver ? "NO ACTION" : "CASCADE"
}),
new ForeignKeyMetadata({
entityMetadata: entityMetadata,
referencedEntityMetadata: parentClosureEntityMetadata,
columns: [entityMetadata.ownColumns[1]],
referencedColumns: parentClosureEntityMetadata.primaryColumns,
// onDelete: "CASCADE" // todo: does not work in mssql for some reason
onDelete: this.connection.driver instanceof SqlServerDriver ? "NO ACTION" : "CASCADE"
}),
];

Expand Down
2 changes: 1 addition & 1 deletion src/metadata-builder/JunctionEntityMetadataBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class JunctionEntityMetadataBuilder {
referencedEntityMetadata: relation.entityMetadata,
columns: junctionColumns,
referencedColumns: referencedColumns,
onDelete: relation.onDelete || "CASCADE"
onDelete: relation.onDelete || "CASCADE"
}),
new ForeignKeyMetadata({
entityMetadata: entityMetadata,
Expand Down
67 changes: 54 additions & 13 deletions src/persistence/SubjectExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export class SubjectExecutor {
* Updates all given subjects in the database.
*/
protected async executeUpdateOperations(): Promise<void> {
await Promise.all(this.updateSubjects.map(async subject => {
const updateSubject = async (subject: Subject) => {

if (!subject.identifier)
throw new SubjectWithoutIdentifierError(subject);
Expand Down Expand Up @@ -408,6 +408,21 @@ export class SubjectExecutor {

const updateMap: ObjectLiteral = subject.createValueSetAndPopChangeMap();

// for tree tables we execute additional queries
switch (subject.metadata.treeType) {
case "nested-set":
await new NestedSetSubjectExecutor(this.queryRunner).update(subject);
break;

case "closure-table":
await new ClosureSubjectExecutor(this.queryRunner).update(subject);
break;

case "materialized-path":
await new MaterializedPathSubjectExecutor(this.queryRunner).update(subject);
break;
}

// here we execute our updation query
// we need to enable entity updation because we update a subject identifier
// which is not same object as our entity that's why we don't need to worry about our entity to get dirty
Expand Down Expand Up @@ -442,20 +457,36 @@ export class SubjectExecutor {
}
Object.assign(subject.generatedMap, updateGeneratedMap);
}
}
};

// experiments, remove probably, need to implement tree tables children removal
// if (subject.updatedRelationMaps.length > 0) {
// await Promise.all(subject.updatedRelationMaps.map(async updatedRelation => {
// if (!updatedRelation.relation.isTreeParent) return;
// if (!updatedRelation.value !== null) return;
//
// if (subject.metadata.treeType === "closure-table") {
// await new ClosureSubjectExecutor(this.queryRunner).deleteChildrenOf(subject);
// }
// }));
// }
// Nested sets need to be updated one by one
// Split array in two, one with nested set subjects and the other with the remaining subjects
const nestedSetSubjects: Subject[] = [];
const remainingSubjects: Subject[] = [];

for (const subject of this.updateSubjects) {
if (subject.metadata.treeType === "nested-set") {
nestedSetSubjects.push(subject);
} else {
remainingSubjects.push(subject);
}
}));
}

// Run nested set updates one by one
const nestedSetPromise = new Promise(async (resolve, reject) => {
for (const subject of nestedSetSubjects) {
try {
await updateSubject(subject);
} catch (error) {
reject(error);
}
}
resolve();
});

// Run all remaning subjects in parallel
await Promise.all([...remainingSubjects.map(updateSubject), nestedSetPromise]);
}

/**
Expand All @@ -482,6 +513,16 @@ export class SubjectExecutor {
await manager.delete(subjects[0].metadata.target, deleteMaps);

} else {
// for tree tables we execute additional queries
switch (subjects[0].metadata.treeType) {
case "nested-set":
await new NestedSetSubjectExecutor(this.queryRunner).remove(subjects);
break;

case "closure-table":
await new ClosureSubjectExecutor(this.queryRunner).remove(subjects);
break;
}

// here we execute our deletion query
// we don't need to specify entities and set update entity to true since the only thing query builder
Expand Down
Loading

0 comments on commit 9c8a3fb

Please sign in to comment.