From 9d2b8e0ef3b54ec96015980a1fe3d691d43d1827 Mon Sep 17 00:00:00 2001 From: Janno Stern Date: Sat, 16 May 2020 18:00:53 +0300 Subject: [PATCH] feat: Add soft remove and recover methods to entity (#5854) * Implement soft remove and recover for entity. * Add test for entity soft remove and recover. * Fix entity soft remove and recover test. --- src/repository/BaseEntity.ts | 31 +++++ .../soft-delete/entity-soft-remove.ts | 106 ++++++++++++++++++ .../repository/soft-delete/entity/Post.ts | 5 +- .../entity/PostWithoutDeleteDateColumn.ts | 5 +- 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 test/functional/repository/soft-delete/entity-soft-remove.ts diff --git a/src/repository/BaseEntity.ts b/src/repository/BaseEntity.ts index e8654eef7f..f1e77cc7e8 100644 --- a/src/repository/BaseEntity.ts +++ b/src/repository/BaseEntity.ts @@ -57,6 +57,20 @@ export class BaseEntity { return (this.constructor as any).getRepository().remove(this, options); } + /** + * Records the delete date of current entity. + */ + softRemove(options?: SaveOptions): Promise { + return (this.constructor as any).getRepository().softRemove(this, options); + } + + /** + * Recovers a given entity in the database. + */ + recover(options?: SaveOptions): Promise { + return (this.constructor as any).getRepository().recover(this, options); + } + /** * Reloads entity data from the database. */ @@ -197,6 +211,23 @@ export class BaseEntity { return (this as any).getRepository().remove(entityOrEntities as any, options); } + /** + * Records the delete date of all given entities. + */ + static softRemove(this: ObjectType, entities: T[], options?: SaveOptions): Promise; + + /** + * Records the delete date of a given entity. + */ + static softRemove(this: ObjectType, entity: T, options?: SaveOptions): Promise; + + /** + * Records the delete date of one or many given entities. + */ + static softRemove(this: ObjectType, entityOrEntities: T|T[], options?: SaveOptions): Promise { + return (this as any).getRepository().softRemove(entityOrEntities as any, options); + } + /** * Inserts a given entity into the database. * Unlike save method executes a primitive operation without cascades, relations and other operations included. diff --git a/test/functional/repository/soft-delete/entity-soft-remove.ts b/test/functional/repository/soft-delete/entity-soft-remove.ts new file mode 100644 index 0000000000..f9c75399e6 --- /dev/null +++ b/test/functional/repository/soft-delete/entity-soft-remove.ts @@ -0,0 +1,106 @@ +import "reflect-metadata"; +import {expect} from "chai"; +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; +import {Connection} from "../../../../src/connection/Connection"; +import {Post} from "./entity/Post"; +import { PostWithoutDeleteDateColumn } from "./entity/PostWithoutDeleteDateColumn"; +import { MissingDeleteDateColumnError } from "../../../../src/error/MissingDeleteDateColumnError"; +import { PromiseUtils } from "../../../../src"; + +describe("entity > soft-remove", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should perform soft removal and recovery correctly", () => PromiseUtils.runInSequence(connections, async connection => { + Post.useConnection(connection); // change connection each time because of AR specifics + + const postRepository = connection.getRepository(Post); + + // save a new posts + const newPost1 = postRepository.create({ + id: 1, + name: "post#1" + }); + const newPost2 = postRepository.create({ + id: 2, + name: "post#2" + }); + + await postRepository.save(newPost1); + await postRepository.save(newPost2); + + // soft-remove one + await newPost1.softRemove(); + + // load to check + const loadedPosts = await postRepository.find({ withDeleted: true }); + + // assert + loadedPosts.length.should.be.equal(2); + + const loadedPost1 = loadedPosts.find(p => p.id === 1); + expect(loadedPost1).to.exist; + expect(loadedPost1!.deletedAt).to.be.instanceof(Date); + expect(loadedPost1!.name).to.equals("post#1"); + const loadedPost2 = loadedPosts.find(p => p.id === 2); + expect(loadedPost2).to.exist; + expect(loadedPost2!.deletedAt).to.equals(null); + expect(loadedPost2!.name).to.equals("post#2"); + + // recover one + await loadedPost1!.recover(); + // load to check + const recoveredPosts = await postRepository.find({ withDeleted: true }); + + // assert + recoveredPosts.length.should.be.equal(2); + + const recoveredPost1 = recoveredPosts.find(p => p.id === 1); + expect(recoveredPost1).to.exist; + expect(recoveredPost1!.deletedAt).to.equals(null); + expect(recoveredPost1!.name).to.equals("post#1"); + const recoveredPost2 = recoveredPosts.find(p => p.id === 2); + expect(recoveredPost2).to.exist; + expect(recoveredPost2!.deletedAt).to.equals(null); + expect(recoveredPost2!.name).to.equals("post#2"); + + })); + + it("should throw error when delete date column is missing", () => PromiseUtils.runInSequence(connections, async connection => { + PostWithoutDeleteDateColumn.useConnection(connection); // change connection each time because of AR specifics + + const postRepository = connection.getRepository(PostWithoutDeleteDateColumn); + + // save a new posts + const newPost1 = postRepository.create({ + id: 1, + name: "post#1" + }); + + await postRepository.save(newPost1); + + let error1: Error | undefined; + try { + // soft-remove one + await newPost1.softRemove(); + } catch (err) { + error1 = err; + } + expect(error1).to.be.an.instanceof(MissingDeleteDateColumnError); + + let error2: Error | undefined; + try { + // recover one + await newPost1.recover(); + } catch (err) { + error2 = err; + } + expect(error2).to.be.an.instanceof(MissingDeleteDateColumnError); + + })); +}); diff --git a/test/functional/repository/soft-delete/entity/Post.ts b/test/functional/repository/soft-delete/entity/Post.ts index 8931d66597..cc4dbea778 100644 --- a/test/functional/repository/soft-delete/entity/Post.ts +++ b/test/functional/repository/soft-delete/entity/Post.ts @@ -2,9 +2,10 @@ import {Entity} from "../../../../../src/decorator/entity/Entity"; import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../../src/decorator/columns/Column"; import {DeleteDateColumn} from "../../../../../src/decorator/columns/DeleteDateColumn"; +import {BaseEntity} from "../../../../../src"; @Entity() -export class Post { +export class Post extends BaseEntity { @PrimaryGeneratedColumn() id: number; @@ -13,4 +14,4 @@ export class Post { @Column() name: string; -} \ No newline at end of file +} diff --git a/test/functional/repository/soft-delete/entity/PostWithoutDeleteDateColumn.ts b/test/functional/repository/soft-delete/entity/PostWithoutDeleteDateColumn.ts index 2001119958..96235d75ff 100644 --- a/test/functional/repository/soft-delete/entity/PostWithoutDeleteDateColumn.ts +++ b/test/functional/repository/soft-delete/entity/PostWithoutDeleteDateColumn.ts @@ -1,12 +1,13 @@ import {Entity} from "../../../../../src/decorator/entity/Entity"; import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../../src/decorator/columns/Column"; +import {BaseEntity} from "../../../../../src"; @Entity() -export class PostWithoutDeleteDateColumn { +export class PostWithoutDeleteDateColumn extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; -} \ No newline at end of file +}