Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add soft remove and recover methods to entity #5854

Merged
merged 3 commits into from May 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}