Skip to content

Commit 9d2b8e0

Browse files
authored
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.
1 parent e584297 commit 9d2b8e0

4 files changed

Lines changed: 143 additions & 4 deletions

File tree

‎src/repository/BaseEntity.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ export class BaseEntity {
5757
return (this.constructor as any).getRepository().remove(this, options);
5858
}
5959

60+
/**
61+
* Records the delete date of current entity.
62+
*/
63+
softRemove(options?: SaveOptions): Promise<this> {
64+
return (this.constructor as any).getRepository().softRemove(this, options);
65+
}
66+
67+
/**
68+
* Recovers a given entity in the database.
69+
*/
70+
recover(options?: SaveOptions): Promise<this> {
71+
return (this.constructor as any).getRepository().recover(this, options);
72+
}
73+
6074
/**
6175
* Reloads entity data from the database.
6276
*/
@@ -197,6 +211,23 @@ export class BaseEntity {
197211
return (this as any).getRepository().remove(entityOrEntities as any, options);
198212
}
199213

214+
/**
215+
* Records the delete date of all given entities.
216+
*/
217+
static softRemove<T extends BaseEntity>(this: ObjectType<T>, entities: T[], options?: SaveOptions): Promise<T[]>;
218+
219+
/**
220+
* Records the delete date of a given entity.
221+
*/
222+
static softRemove<T extends BaseEntity>(this: ObjectType<T>, entity: T, options?: SaveOptions): Promise<T>;
223+
224+
/**
225+
* Records the delete date of one or many given entities.
226+
*/
227+
static softRemove<T extends BaseEntity>(this: ObjectType<T>, entityOrEntities: T|T[], options?: SaveOptions): Promise<T|T[]> {
228+
return (this as any).getRepository().softRemove(entityOrEntities as any, options);
229+
}
230+
200231
/**
201232
* Inserts a given entity into the database.
202233
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import "reflect-metadata";
2+
import {expect} from "chai";
3+
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
4+
import {Connection} from "../../../../src/connection/Connection";
5+
import {Post} from "./entity/Post";
6+
import { PostWithoutDeleteDateColumn } from "./entity/PostWithoutDeleteDateColumn";
7+
import { MissingDeleteDateColumnError } from "../../../../src/error/MissingDeleteDateColumnError";
8+
import { PromiseUtils } from "../../../../src";
9+
10+
describe("entity > soft-remove", () => {
11+
12+
let connections: Connection[];
13+
before(async () => connections = await createTestingConnections({
14+
entities: [__dirname + "/entity/*{.js,.ts}"],
15+
}));
16+
beforeEach(() => reloadTestingDatabases(connections));
17+
after(() => closeTestingConnections(connections));
18+
19+
it("should perform soft removal and recovery correctly", () => PromiseUtils.runInSequence(connections, async connection => {
20+
Post.useConnection(connection); // change connection each time because of AR specifics
21+
22+
const postRepository = connection.getRepository(Post);
23+
24+
// save a new posts
25+
const newPost1 = postRepository.create({
26+
id: 1,
27+
name: "post#1"
28+
});
29+
const newPost2 = postRepository.create({
30+
id: 2,
31+
name: "post#2"
32+
});
33+
34+
await postRepository.save(newPost1);
35+
await postRepository.save(newPost2);
36+
37+
// soft-remove one
38+
await newPost1.softRemove();
39+
40+
// load to check
41+
const loadedPosts = await postRepository.find({ withDeleted: true });
42+
43+
// assert
44+
loadedPosts.length.should.be.equal(2);
45+
46+
const loadedPost1 = loadedPosts.find(p => p.id === 1);
47+
expect(loadedPost1).to.exist;
48+
expect(loadedPost1!.deletedAt).to.be.instanceof(Date);
49+
expect(loadedPost1!.name).to.equals("post#1");
50+
const loadedPost2 = loadedPosts.find(p => p.id === 2);
51+
expect(loadedPost2).to.exist;
52+
expect(loadedPost2!.deletedAt).to.equals(null);
53+
expect(loadedPost2!.name).to.equals("post#2");
54+
55+
// recover one
56+
await loadedPost1!.recover();
57+
// load to check
58+
const recoveredPosts = await postRepository.find({ withDeleted: true });
59+
60+
// assert
61+
recoveredPosts.length.should.be.equal(2);
62+
63+
const recoveredPost1 = recoveredPosts.find(p => p.id === 1);
64+
expect(recoveredPost1).to.exist;
65+
expect(recoveredPost1!.deletedAt).to.equals(null);
66+
expect(recoveredPost1!.name).to.equals("post#1");
67+
const recoveredPost2 = recoveredPosts.find(p => p.id === 2);
68+
expect(recoveredPost2).to.exist;
69+
expect(recoveredPost2!.deletedAt).to.equals(null);
70+
expect(recoveredPost2!.name).to.equals("post#2");
71+
72+
}));
73+
74+
it("should throw error when delete date column is missing", () => PromiseUtils.runInSequence(connections, async connection => {
75+
PostWithoutDeleteDateColumn.useConnection(connection); // change connection each time because of AR specifics
76+
77+
const postRepository = connection.getRepository(PostWithoutDeleteDateColumn);
78+
79+
// save a new posts
80+
const newPost1 = postRepository.create({
81+
id: 1,
82+
name: "post#1"
83+
});
84+
85+
await postRepository.save(newPost1);
86+
87+
let error1: Error | undefined;
88+
try {
89+
// soft-remove one
90+
await newPost1.softRemove();
91+
} catch (err) {
92+
error1 = err;
93+
}
94+
expect(error1).to.be.an.instanceof(MissingDeleteDateColumnError);
95+
96+
let error2: Error | undefined;
97+
try {
98+
// recover one
99+
await newPost1.recover();
100+
} catch (err) {
101+
error2 = err;
102+
}
103+
expect(error2).to.be.an.instanceof(MissingDeleteDateColumnError);
104+
105+
}));
106+
});

‎test/functional/repository/soft-delete/entity/Post.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import {Entity} from "../../../../../src/decorator/entity/Entity";
22
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
33
import {Column} from "../../../../../src/decorator/columns/Column";
44
import {DeleteDateColumn} from "../../../../../src/decorator/columns/DeleteDateColumn";
5+
import {BaseEntity} from "../../../../../src";
56

67
@Entity()
7-
export class Post {
8+
export class Post extends BaseEntity {
89
@PrimaryGeneratedColumn()
910
id: number;
1011

@@ -13,4 +14,4 @@ export class Post {
1314

1415
@Column()
1516
name: string;
16-
}
17+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {Entity} from "../../../../../src/decorator/entity/Entity";
22
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
33
import {Column} from "../../../../../src/decorator/columns/Column";
4+
import {BaseEntity} from "../../../../../src";
45

56
@Entity()
6-
export class PostWithoutDeleteDateColumn {
7+
export class PostWithoutDeleteDateColumn extends BaseEntity {
78
@PrimaryGeneratedColumn()
89
id: number;
910

1011
@Column()
1112
name: string;
12-
}
13+
}

0 commit comments

Comments
 (0)