Skip to content

Commit

Permalink
feat: Add soft remove and recover methods to entity (#5854)
Browse files Browse the repository at this point in the history
* Implement soft remove and recover for entity.

* Add test for entity soft remove and recover.

* Fix entity soft remove and recover test.
  • Loading branch information
jannostern committed May 16, 2020
1 parent e584297 commit 9d2b8e0
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 4 deletions.
31 changes: 31 additions & 0 deletions src/repository/BaseEntity.ts
Expand Up @@ -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<this> {
return (this.constructor as any).getRepository().softRemove(this, options);
}

/**
* Recovers a given entity in the database.
*/
recover(options?: SaveOptions): Promise<this> {
return (this.constructor as any).getRepository().recover(this, options);
}

/**
* Reloads entity data from the database.
*/
Expand Down Expand Up @@ -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<T extends BaseEntity>(this: ObjectType<T>, entities: T[], options?: SaveOptions): Promise<T[]>;

/**
* Records the delete date of a given entity.
*/
static softRemove<T extends BaseEntity>(this: ObjectType<T>, entity: T, options?: SaveOptions): Promise<T>;

/**
* Records the delete date of one or many given entities.
*/
static softRemove<T extends BaseEntity>(this: ObjectType<T>, entityOrEntities: T|T[], options?: SaveOptions): Promise<T|T[]> {
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.
Expand Down
106 changes: 106 additions & 0 deletions 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);

}));
});
5 changes: 3 additions & 2 deletions test/functional/repository/soft-delete/entity/Post.ts
Expand Up @@ -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;

Expand All @@ -13,4 +14,4 @@ export class Post {

@Column()
name: string;
}
}
@@ -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;
}
}

0 comments on commit 9d2b8e0

Please sign in to comment.