mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: allow soft-deletion of orphaned relation rows using orphanedRow… (#8414)
* feat: Allow soft-deletion of orphaned relation rows using orphanedRowAction * add deletedate column * fix: create docs * improve the tests * remove .only in the tests file Co-authored-by: oxeye-yuvalk <oxeye-yuvalk@oxeye.io>
This commit is contained in:
parent
2834729e80
commit
cefddd95c5
@ -25,7 +25,7 @@ There are several options you can specify for relations:
|
||||
* `onDelete: "RESTRICT"|"CASCADE"|"SET NULL"` - specifies how foreign key should behave when referenced object is deleted
|
||||
* `primary: boolean` - Indicates whether this relation's column will be a primary column or not.
|
||||
* `nullable: boolean` - Indicates whether this relation's column is nullable or not. By default it is nullable.
|
||||
* `orphanedRowAction: "nullify" | "delete"` - When a child row is removed from its parent, determines if the child row should be orphaned (default) or deleted.
|
||||
* `orphanedRowAction: "nullify" | "delete" | "soft-delete"` - When a child row is removed from its parent, determines if the child row should be orphaned (default) or deleted (delete or soft delete).
|
||||
|
||||
## Cascades
|
||||
|
||||
|
||||
@ -74,6 +74,6 @@ export interface RelationOptions {
|
||||
/**
|
||||
* When a child row is removed from its parent, determines if the child row should be orphaned (default) or deleted.
|
||||
*/
|
||||
orphanedRowAction?: "nullify" | "delete";
|
||||
orphanedRowAction?: "nullify" | "delete" | "soft-delete";
|
||||
|
||||
}
|
||||
|
||||
@ -102,5 +102,5 @@ export interface EntitySchemaRelationOptions {
|
||||
/**
|
||||
* When a child row is removed from its parent, determines if the child row should be orphaned (default) or deleted.
|
||||
*/
|
||||
orphanedRowAction?: "nullify" | "delete";
|
||||
orphanedRowAction?: "nullify" | "delete" | "soft-delete";
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@ export class RelationMetadata {
|
||||
/**
|
||||
* When a child row is removed from its parent, determines if the child row should be orphaned (default) or deleted.
|
||||
*/
|
||||
orphanedRowAction?: "nullify" | "delete";
|
||||
orphanedRowAction?: "nullify" | "delete" | "soft-delete";
|
||||
|
||||
/**
|
||||
* If set to true then related objects are allowed to be inserted to the database.
|
||||
|
||||
@ -184,7 +184,10 @@ export class OneToManySubjectBuilder {
|
||||
} else if (relation.inverseRelation.orphanedRowAction === "delete") {
|
||||
removedRelatedEntitySubject.mustBeRemoved = true;
|
||||
}
|
||||
|
||||
else if (relation.inverseRelation.orphanedRowAction === "soft-delete") {
|
||||
removedRelatedEntitySubject.canBeSoftRemoved = true;
|
||||
}
|
||||
|
||||
this.subjects.push(removedRelatedEntitySubject);
|
||||
});
|
||||
}
|
||||
|
||||
75
test/github-issues/8408/delete-orphans.ts
Normal file
75
test/github-issues/8408/delete-orphans.ts
Normal file
@ -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";
|
||||
|
||||
describe("persistence > delete orphans", () => {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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 categoryId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
await Promise.all(connections.map(async connection => {
|
||||
categoryRepository = connection.getRepository(Category);
|
||||
postRepository = connection.getRepository(Post);
|
||||
}));
|
||||
|
||||
const categoryToInsert = await categoryRepository.save(new Category());
|
||||
categoryToInsert.posts = [
|
||||
new Post(),
|
||||
new Post()
|
||||
];
|
||||
|
||||
await categoryRepository.save(categoryToInsert);
|
||||
categoryId = categoryToInsert.id;
|
||||
|
||||
const categoryToUpdate = (await categoryRepository.findOne(categoryId))!;
|
||||
categoryToUpdate.posts = categoryToInsert.posts.filter(p => p.id === 1); // Keep the first post
|
||||
|
||||
await categoryRepository.save(categoryToUpdate);
|
||||
});
|
||||
|
||||
it("should retain a Post on the Category", async () => {
|
||||
const category = await categoryRepository.findOne(categoryId);
|
||||
expect(category).not.to.be.undefined;
|
||||
expect(category!.posts).to.have.lengthOf(1);
|
||||
expect(category!.posts[0].id).to.equal(1);
|
||||
});
|
||||
|
||||
it("should mark orphaned Post as soft-deleted", async () => {
|
||||
const postCount = await postRepository.count();
|
||||
expect(postCount).to.equal(1);
|
||||
const postCountIncludeDeleted = await postRepository.count({withDeleted: true});
|
||||
expect(postCountIncludeDeleted).to.equal(2);
|
||||
});
|
||||
|
||||
it("should retain foreign keys on remaining Posts", async () => {
|
||||
const postsWithoutForeignKeys = (await postRepository.find())
|
||||
.filter(p => !p.categoryId);
|
||||
expect(postsWithoutForeignKeys).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
21
test/github-issues/8408/entity/Category.ts
Normal file
21
test/github-issues/8408/entity/Category.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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,
|
||||
eager: true
|
||||
})
|
||||
posts: Post[];
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
}
|
||||
24
test/github-issues/8408/entity/Post.ts
Normal file
24
test/github-issues/8408/entity/Post.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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} from "../../../../src";
|
||||
|
||||
@Entity()
|
||||
export class Post {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
categoryId: string;
|
||||
|
||||
@ManyToOne(() => Category, category => category.posts, { orphanedRowAction: "soft-delete" })
|
||||
@JoinColumn({ name: "categoryId" })
|
||||
category: Category;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user