Skip to content

Commit

Permalink
feat: soft delete recursive cascade (#8436)
Browse files Browse the repository at this point in the history
* feat: soft-delete-recursive-cascade

* fix get entity primary key

* better mapping for get parent ids

* fix use identifier and not entity from subject object

* remove only from test

* fix: replace primary key name to valid name

* fix: test bug

* fix: change returning to work on mssql

* fix: work for oracle

* add comments to subject executor

Co-authored-by: oxeye-yuvalk <oxeye-yuvalk@oxeye.io>
  • Loading branch information
oxeye-yuvalk and oxeye-yuvalk committed Feb 16, 2022
1 parent 3d6c5da commit d0f32b3
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 2 deletions.
44 changes: 42 additions & 2 deletions src/persistence/SubjectExecutor.ts
Expand Up @@ -18,6 +18,7 @@ import {ClosureSubjectExecutor} from "./tree/ClosureSubjectExecutor";
import {MaterializedPathSubjectExecutor} from "./tree/MaterializedPathSubjectExecutor";
import {OrmUtils} from "../util/OrmUtils";
import { UpdateResult } from "../query-builder/result/UpdateResult";
import {RelationMetadata} from "../metadata/RelationMetadata";

/**
* Executes all database operations (inserts, updated, deletes) that must be executed
Expand Down Expand Up @@ -607,8 +608,12 @@ export class SubjectExecutor {
} else { // in this case identifier is just conditions object to update by
softDeleteQueryBuilder.where(subject.identifier);
}

updateResult = await softDeleteQueryBuilder.execute();
// Move throw all the relation of the subject
for (const relation of subject.metadata.relations) {
// Call recursive function that get the parents primary keys that in used on the inverse side in all one to many relations
await this.executeSoftRemoveRecursive(relation, [Reflect.get(subject.identifier, subject.metadata.primaryColumns[0].propertyName)]);
}
}

subject.generatedMap = updateResult.generatedMaps[0];
Expand All @@ -635,10 +640,45 @@ export class SubjectExecutor {
// }
}));
}

/**
* Recovers all given subjects in the database.
*/

protected async executeSoftRemoveRecursive(relation: RelationMetadata, ids: any[]): Promise<void> {
// We want to delete the entities just when the relation is cascade soft remove
if (relation.isCascadeSoftRemove){

let primaryPropertyName = relation.inverseEntityMetadata.primaryColumns[0].propertyName;
let updateResult: UpdateResult;
let softDeleteQueryBuilder = this.queryRunner
.manager
.createQueryBuilder()
.softDelete()
.from(relation.inverseEntityMetadata.target)
// We get back list of the affected rows primary keys for call again
.returning([primaryPropertyName])
.updateEntity(this.options && this.options.reload === false ? false : true)
.callListeners(false);
// soft remove only where parent id is in the list
softDeleteQueryBuilder.where(`${relation.inverseSidePropertyPath} in (:...ids)`, {ids: ids});
updateResult = await softDeleteQueryBuilder.execute();
let parentIds;
// Only in oracle the returning value is a list of the affected row primary keys and not list of dictionary
if (this.queryRunner.connection.driver instanceof OracleDriver){
parentIds = updateResult.raw[0];
}
else {
parentIds = updateResult.raw.map((row: any) => row[Object.keys(row)[0]]);
}
if (parentIds.length) {
// This is the recursive - check the relations of the relation
for (const subRelation of relation.inverseEntityMetadata.relations) {
await this.executeSoftRemoveRecursive(subRelation, parentIds);
}
}
}
}

protected async executeRecoverOperations(): Promise<void> {
await Promise.all(this.recoverSubjects.map(async subject => {

Expand Down
75 changes: 75 additions & 0 deletions test/github-issues/8416/8416.ts
@@ -0,0 +1,75 @@
import "reflect-metadata";
import { Connection, Repository } from "../../../src/index";
import { reloadTestingDatabases, createTestingConnections, closeTestingConnections } from "../../utils/test-utils";
import { expect } from "chai";
import { Category } from "./entity/Category";
import { Post } from "./entity/Post";
import {Author} from "./entity/Author";

describe("Soft Delete Recursive cascade", () => {

// -------------------------------------------------------------------------
// Configuration
// -------------------------------------------------------------------------

// connect to db
let connections: Connection[] = [];

before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],

}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

// -------------------------------------------------------------------------
// Specifications
// -------------------------------------------------------------------------

describe("when a Post is removed from a Category", () => {
let categoryRepository: Repository<Category>;
let postRepository: Repository<Post>;
let authorRepository: Repository<Author>;

beforeEach(async () => {
await Promise.all(connections.map(async connection => {
categoryRepository = connection.getRepository(Category);
postRepository = connection.getRepository(Post);
authorRepository = connection.getRepository(Author);
}));
const firstPost: Post = new Post();
firstPost.authors = [
new Author(),
new Author()
];
const secondPost: Post = new Post();
secondPost.authors = [
new Author(),
new Author()
];
const categoryToInsert = new Category();
categoryToInsert.posts = [
firstPost,
secondPost
];

await categoryRepository.save(categoryToInsert);
let insertedCategory: Category = await categoryRepository.findOneOrFail();
await categoryRepository.softRemove(insertedCategory);
});

it("should delete the category", async () => {
const categoryCount = await categoryRepository.count();
expect(categoryCount).to.equal(0);
});

it("should delete the all the posts", async () => {
const postCount = await postRepository.count();
expect(postCount).to.equal(0);
});
it("should delete the all the authors", async () => {
const authorsCount = await authorRepository.count();
expect(authorsCount).to.equal(0);
});
});
});
24 changes: 24 additions & 0 deletions test/github-issues/8416/entity/Author.ts
@@ -0,0 +1,24 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";
import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne";
import {JoinColumn} from "../../../../src/decorator/relations/JoinColumn";
import {DeleteDateColumn} from "../../../../src";
import {Post} from "./Post";

@Entity()
export class Author {

@PrimaryGeneratedColumn()
id: number;

@Column()
postId: string;

@ManyToOne(() => Post, post => post.authors)
@JoinColumn({ name: "postId" })
post: Post;

@DeleteDateColumn()
deletedAt?: Date;
}
20 changes: 20 additions & 0 deletions test/github-issues/8416/entity/Category.ts
@@ -0,0 +1,20 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Post} from "./Post";
import {OneToMany} from "../../../../src/decorator/relations/OneToMany";
import {DeleteDateColumn} from "../../../../src";

@Entity()
export class Category {

@PrimaryGeneratedColumn()
id: number;

@OneToMany(() => Post, post => post.category, {
cascade: true
})
posts: Post[];

@DeleteDateColumn()
deletedAt?: Date;
}
30 changes: 30 additions & 0 deletions test/github-issues/8416/entity/Post.ts
@@ -0,0 +1,30 @@
import {Category} from "./Category";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";
import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne";
import {JoinColumn} from "../../../../src/decorator/relations/JoinColumn";
import {DeleteDateColumn, OneToMany} from "../../../../src";
import {Author} from "./Author";

@Entity()
export class Post {

@PrimaryGeneratedColumn()
id: number;

@Column()
categoryId: string;

@ManyToOne(() => Category, category => category.posts)
@JoinColumn({ name: "categoryId" })
category: Category;

@OneToMany(() => Author, author => author.post, {
cascade: true
})
authors: Author[];

@DeleteDateColumn()
deletedAt?: Date;
}

0 comments on commit d0f32b3

Please sign in to comment.