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

Feat/soft delete #5034

Merged
merged 31 commits into from Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ed19a06
added @DeleteDateColumn
Oct 26, 2019
acda793
updated test for embedded-with-special-columns
Oct 26, 2019
8032251
added the softDelete and restore methods to QueryBuilder
Oct 26, 2019
725c7e4
added test for query builder > soft-delete
Oct 26, 2019
66ab9b0
added the softDelete and restore methods to repository
Oct 27, 2019
e320638
added test for repository > soft-delete and restore
Oct 27, 2019
9a0e7ed
added the softRemove and recover methods to repository
Nov 1, 2019
a3083d4
added test for repository > soft-remove and recover
Nov 1, 2019
181fe21
fixed the title of the test for repository > soft-delete
Nov 1, 2019
b79d2a2
added the support of the cascades soft-remove and recover
Nov 1, 2019
40258d3
added test: support of the cascades soft-remove and recover
Nov 1, 2019
05e6ff9
fixed test for should perform restory with limit correctly: missing a…
Nov 5, 2019
2ada3c5
fixed the wrong comment for recover operation
Feb 1, 2020
1602f00
added the global condition of non-deleted to query-builder for the en…
Feb 1, 2020
8a47615
added the global condition of non-deleted to repository for the entit…
Feb 1, 2020
2771c9f
updated test for the global condition of non-deleted
Feb 1, 2020
5bb263a
added the test for soft-delete and restore properties inside embeds a…
Feb 1, 2020
4f2ba23
added test to query-builder for the global condition of non-deleted
Feb 1, 2020
6998d91
updated test to repository for the global condition of non-deleted
Feb 1, 2020
c638ab0
added test to repository for the global condition of non-deleted
Feb 1, 2020
fd168ec
Merge pull request #1 from iWinston/feat/global-condition-non-deleted
iWinston Feb 1, 2020
e6c63b7
fixed comment for the test 'find with the global condition of non-del…
Feb 2, 2020
1961b09
fixed can't add the corrent global condition as the missing of aliasN…
Feb 11, 2020
faf4abd
fixed can't get the correct result as the missing of the ordering by id
Feb 11, 2020
89a0b78
Merge pull request #2 from iWinston/feat/global-condition-non-deleted
iWinston Feb 11, 2020
d2b7c64
fixed should use propertyName instead of databaseName
Feb 13, 2020
997aef0
Merge pull request #3 from iWinston/feat/global-condition-non-deleted
iWinston Feb 13, 2020
990b88d
Merge pull request #4 from typeorm/master
iWinston Feb 16, 2020
b753752
Merge pull request #5 from iWinston/master
iWinston Feb 18, 2020
9ae0761
added deleteDate and deleteDateNullable for sap
Feb 18, 2020
82fc039
Merge branch 'master' into feat/soft-delete
iWinston Feb 18, 2020
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
17 changes: 17 additions & 0 deletions src/decorator/columns/DeleteDateColumn.ts
@@ -0,0 +1,17 @@
import { ColumnOptions, getMetadataArgsStorage } from "../../";
import { ColumnMetadataArgs } from "../../metadata-args/ColumnMetadataArgs";

/**
* This column will store a delete date of the soft-deleted object.
* This date is being updated each time you soft-delete the object.
*/
export function DeleteDateColumn(options?: ColumnOptions): Function {
return function(object: Object, propertyName: string) {
getMetadataArgsStorage().columns.push({
target: object.constructor,
propertyName: propertyName,
mode: "deleteDate",
options: options || {}
} as ColumnMetadataArgs);
};
}
4 changes: 2 additions & 2 deletions src/decorator/options/RelationOptions.ts
Expand Up @@ -12,9 +12,9 @@ export interface RelationOptions {
* If set to true then it means that related object can be allowed to be inserted or updated in the database.
* You can separately restrict cascades to insertion or updation using following syntax:
*
* cascade: ["insert", "update"] // include or exclude one of them
* cascade: ["insert", "update", "remove", "soft-remove", "recover"] // include or exclude one of them
*/
cascade?: boolean|("insert"|"update"|"remove")[];
cascade?: boolean|("insert"|"update"|"remove"|"soft-remove"|"recover")[];

/**
* Indicates if relation column value can be nullable or not.
Expand Down
4 changes: 2 additions & 2 deletions src/decorator/tree/TreeChildren.ts
Expand Up @@ -5,15 +5,15 @@ import {RelationMetadataArgs} from "../../metadata-args/RelationMetadataArgs";
* Marks a entity property as a children of the tree.
* "Tree children" will contain all children (bind) of this entity.
*/
export function TreeChildren(options?: { cascade?: boolean|("insert"|"update"|"remove")[] }): Function {
export function TreeChildren(options?: { cascade?: boolean|("insert"|"update"|"remove"|"soft-remove"|"recover")[] }): Function {
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;

// now try to determine it its lazy relation
const reflectedType = Reflect && (Reflect as any).getMetadata ? Reflect.getMetadata("design:type", object, propertyName) : undefined;
const isLazy = (reflectedType && typeof reflectedType.name === "string" && reflectedType.name.toLowerCase() === "promise") || false;

// add one-to-many relation for this
// add one-to-many relation for this
getMetadataArgsStorage().relations.push({
isTreeChildren: true,
target: object.constructor,
Expand Down
3 changes: 3 additions & 0 deletions src/driver/aurora-data-api/AuroraDataApiDriver.ts
Expand Up @@ -231,6 +231,9 @@ export class AuroraDataApiDriver implements Driver {
updateDate: "datetime",
updateDatePrecision: 6,
updateDateDefault: "CURRENT_TIMESTAMP(6)",
deleteDate: "datetime",
deleteDatePrecision: 6,
deleteDateNullable: true,
version: "int",
treeLevel: "int",
migrationId: "int",
Expand Down
2 changes: 2 additions & 0 deletions src/driver/cockroachdb/CockroachDriver.ts
Expand Up @@ -172,6 +172,8 @@ export class CockroachDriver implements Driver {
createDateDefault: "now()",
updateDate: "timestamptz",
updateDateDefault: "now()",
deleteDate: "timestamptz",
deleteDateNullable: true,
version: Number,
treeLevel: Number,
migrationId: Number,
Expand Down
2 changes: 2 additions & 0 deletions src/driver/mongodb/MongoDriver.ts
Expand Up @@ -94,6 +94,8 @@ export class MongoDriver implements Driver {
createDateDefault: "",
updateDate: "int",
updateDateDefault: "",
deleteDate: "int",
deleteDateNullable: true,
version: "int",
treeLevel: "int",
migrationId: "int",
Expand Down
3 changes: 3 additions & 0 deletions src/driver/mysql/MysqlDriver.ts
Expand Up @@ -236,6 +236,9 @@ export class MysqlDriver implements Driver {
updateDate: "datetime",
updateDatePrecision: 6,
updateDateDefault: "CURRENT_TIMESTAMP(6)",
deleteDate: "datetime",
deleteDatePrecision: 6,
deleteDateNullable: true,
version: "int",
treeLevel: "int",
migrationId: "int",
Expand Down
2 changes: 2 additions & 0 deletions src/driver/oracle/OracleDriver.ts
Expand Up @@ -155,6 +155,8 @@ export class OracleDriver implements Driver {
createDateDefault: "CURRENT_TIMESTAMP",
updateDate: "timestamp",
updateDateDefault: "CURRENT_TIMESTAMP",
deleteDate: "timestamp",
deleteDateNullable: true,
version: "number",
treeLevel: "number",
migrationId: "number",
Expand Down
2 changes: 2 additions & 0 deletions src/driver/postgres/PostgresDriver.ts
Expand Up @@ -203,6 +203,8 @@ export class PostgresDriver implements Driver {
createDateDefault: "now()",
updateDate: "timestamp",
updateDateDefault: "now()",
deleteDate: "timestamp",
deleteDateNullable: true,
version: "int4",
treeLevel: "int4",
migrationId: "int4",
Expand Down
2 changes: 2 additions & 0 deletions src/driver/sap/SapDriver.ts
Expand Up @@ -148,6 +148,8 @@ export class SapDriver implements Driver {
createDateDefault: "CURRENT_TIMESTAMP",
updateDate: "timestamp",
updateDateDefault: "CURRENT_TIMESTAMP",
deleteDate: "timestamp",
deleteDateNullable: true,
version: "integer",
treeLevel: "integer",
migrationId: "integer",
Expand Down
2 changes: 2 additions & 0 deletions src/driver/sqlite-abstract/AbstractSqliteDriver.ts
Expand Up @@ -146,6 +146,8 @@ export abstract class AbstractSqliteDriver implements Driver {
createDateDefault: "datetime('now')",
updateDate: "datetime",
updateDateDefault: "datetime('now')",
deleteDate: "datetime",
deleteDateNullable: true,
version: "integer",
treeLevel: "integer",
migrationId: "integer",
Expand Down
2 changes: 2 additions & 0 deletions src/driver/sqlserver/SqlServerDriver.ts
Expand Up @@ -164,6 +164,8 @@ export class SqlServerDriver implements Driver {
createDateDefault: "getdate()",
updateDate: "datetime2",
updateDateDefault: "getdate()",
deleteDate: "datetime2",
deleteDateNullable: true,
version: "int",
treeLevel: "int",
migrationId: "int",
Expand Down
15 changes: 15 additions & 0 deletions src/driver/types/MappedColumnTypes.ts
Expand Up @@ -36,6 +36,21 @@ export interface MappedColumnTypes {
*/
updateDateDefault: string;

/**
* Column type for the delete date column.
*/
deleteDate: ColumnType;

/**
* Precision of datetime column. Used in MySql to define milliseconds.
*/
deleteDatePrecision?: number;

/**
* Nullable value should be used by a database for "deleted date" column.
*/
deleteDateNullable: boolean;

/**
* Column type for the version column.
*/
Expand Down
182 changes: 182 additions & 0 deletions src/entity-manager/EntityManager.ts
Expand Up @@ -467,6 +467,112 @@ export class EntityManager {
.then(() => entity);
}

/**
* Records the delete date of all given entities.
*/
softRemove<Entity>(entities: Entity[], options?: SaveOptions): Promise<Entity[]>;

/**
* Records the delete date of a given entity.
*/
softRemove<Entity>(entity: Entity, options?: SaveOptions): Promise<Entity>;

/**
* Records the delete date of all given entities.
*/
softRemove<Entity, T extends DeepPartial<Entity>>(targetOrEntity: ObjectType<Entity>|EntitySchema<Entity>, entities: T[], options?: SaveOptions): Promise<T[]>;

/**
* Records the delete date of a given entity.
*/
softRemove<Entity, T extends DeepPartial<Entity>>(targetOrEntity: ObjectType<Entity>|EntitySchema<Entity>, entity: T, options?: SaveOptions): Promise<T>;

/**
* Records the delete date of all given entities.
*/
softRemove<T>(targetOrEntity: string, entities: T[], options?: SaveOptions): Promise<T[]>;

/**
* Records the delete date of a given entity.
*/
softRemove<T>(targetOrEntity: string, entity: T, options?: SaveOptions): Promise<T>;

/**
* Records the delete date of one or many given entities.
*/
softRemove<Entity, T extends DeepPartial<Entity>>(targetOrEntity: (T|T[])|ObjectType<Entity>|EntitySchema<Entity>|string, maybeEntityOrOptions?: T|T[], maybeOptions?: SaveOptions): Promise<T|T[]> {

// normalize mixed parameters
let target = (arguments.length > 1 && (targetOrEntity instanceof Function || targetOrEntity instanceof EntitySchema || typeof targetOrEntity === "string")) ? targetOrEntity as Function|string : undefined;
const entity: T|T[] = target ? maybeEntityOrOptions as T|T[] : targetOrEntity as T|T[];
const options = target ? maybeOptions : maybeEntityOrOptions as SaveOptions;

if (target instanceof EntitySchema)
target = target.options.name;

// if user passed empty array of entities then we don't need to do anything
if (entity instanceof Array && entity.length === 0)
return Promise.resolve(entity);

// execute soft-remove operation
return new EntityPersistExecutor(this.connection, this.queryRunner, "soft-remove", target, entity, options)
.execute()
.then(() => entity);
}

/**
* Recovers all given entities.
*/
recover<Entity>(entities: Entity[], options?: SaveOptions): Promise<Entity[]>;

/**
* Recovers a given entity.
*/
recover<Entity>(entity: Entity, options?: SaveOptions): Promise<Entity>;

/**
* Recovers all given entities.
*/
recover<Entity, T extends DeepPartial<Entity>>(targetOrEntity: ObjectType<Entity>|EntitySchema<Entity>, entities: T[], options?: SaveOptions): Promise<T[]>;

/**
* Recovers a given entity.
*/
recover<Entity, T extends DeepPartial<Entity>>(targetOrEntity: ObjectType<Entity>|EntitySchema<Entity>, entity: T, options?: SaveOptions): Promise<T>;

/**
* Recovers all given entities.
*/
recover<T>(targetOrEntity: string, entities: T[], options?: SaveOptions): Promise<T[]>;

/**
* Recovers a given entity.
*/
recover<T>(targetOrEntity: string, entity: T, options?: SaveOptions): Promise<T>;

/**
* Recovers one or many given entities.
*/
recover<Entity, T extends DeepPartial<Entity>>(targetOrEntity: (T|T[])|ObjectType<Entity>|EntitySchema<Entity>|string, maybeEntityOrOptions?: T|T[], maybeOptions?: SaveOptions): Promise<T|T[]> {

// normalize mixed parameters
let target = (arguments.length > 1 && (targetOrEntity instanceof Function || targetOrEntity instanceof EntitySchema || typeof targetOrEntity === "string")) ? targetOrEntity as Function|string : undefined;
const entity: T|T[] = target ? maybeEntityOrOptions as T|T[] : targetOrEntity as T|T[];
const options = target ? maybeOptions : maybeEntityOrOptions as SaveOptions;

if (target instanceof EntitySchema)
target = target.options.name;

// if user passed empty array of entities then we don't need to do anything
if (entity instanceof Array && entity.length === 0)
return Promise.resolve(entity);

// execute recover operation
return new EntityPersistExecutor(this.connection, this.queryRunner, "recover", target, entity, options)
.execute()
.then(() => entity);
}

/**
* Inserts a given entity into the database.
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
Expand Down Expand Up @@ -564,6 +670,82 @@ export class EntityManager {
}
}

/**
* Records the delete date of entities by a given condition(s).
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient DELETE query.
* Does not check if entity exist in the database.
* Condition(s) cannot be empty.
*/
softDelete<Entity>(targetOrEntity: ObjectType<Entity>|EntitySchema<Entity>|string, criteria: string|string[]|number|number[]|Date|Date[]|ObjectID|ObjectID[]|any): Promise<UpdateResult> {

// if user passed empty criteria or empty list of criterias, then throw an error
if (criteria === undefined ||
criteria === null ||
criteria === "" ||
(criteria instanceof Array && criteria.length === 0)) {

return Promise.reject(new Error(`Empty criteria(s) are not allowed for the delete method.`));
}

if (typeof criteria === "string" ||
typeof criteria === "number" ||
criteria instanceof Date ||
criteria instanceof Array) {

return this.createQueryBuilder()
.softDelete()
.from(targetOrEntity)
.whereInIds(criteria)
.execute();

} else {
return this.createQueryBuilder()
.softDelete()
.from(targetOrEntity)
.where(criteria)
.execute();
}
}

/**
* Restores entities by a given condition(s).
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient DELETE query.
* Does not check if entity exist in the database.
* Condition(s) cannot be empty.
*/
restore<Entity>(targetOrEntity: ObjectType<Entity>|EntitySchema<Entity>|string, criteria: string|string[]|number|number[]|Date|Date[]|ObjectID|ObjectID[]|any): Promise<UpdateResult> {

// if user passed empty criteria or empty list of criterias, then throw an error
if (criteria === undefined ||
criteria === null ||
criteria === "" ||
(criteria instanceof Array && criteria.length === 0)) {

return Promise.reject(new Error(`Empty criteria(s) are not allowed for the delete method.`));
}

if (typeof criteria === "string" ||
typeof criteria === "number" ||
criteria instanceof Date ||
criteria instanceof Array) {

return this.createQueryBuilder()
.restore()
.from(targetOrEntity)
.whereInIds(criteria)
.execute();

} else {
return this.createQueryBuilder()
.restore()
.from(targetOrEntity)
.where(criteria)
.execute();
}
}

/**
* Counts entities that match given options.
* Useful for pagination.
Expand Down
5 changes: 5 additions & 0 deletions src/entity-schema/EntitySchemaColumnOptions.ts
Expand Up @@ -24,6 +24,11 @@ export interface EntitySchemaColumnOptions extends SpatialColumnOptions {
*/
updateDate?: boolean;

/**
* Indicates if this column is a delete date column.
*/
deleteDate?: boolean;

/**
* Indicates if this column is a version column.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/entity-schema/EntitySchemaRelationOptions.ts
Expand Up @@ -71,7 +71,7 @@ export interface EntitySchemaRelationOptions {
* If set to true then it means that related object can be allowed to be inserted / updated / removed to the db.
* This is option a shortcut if you would like to set cascadeInsert, cascadeUpdate and cascadeRemove to true.
*/
cascade?: boolean|("insert"|"update"|"remove")[];
cascade?: boolean|("insert"|"update"|"remove"|"soft-remove"|"recover")[];

/**
* Default database value.
Expand Down
2 changes: 2 additions & 0 deletions src/entity-schema/EntitySchemaTransformer.ts
Expand Up @@ -54,6 +54,8 @@ export class EntitySchemaTransformer {
mode = "createDate";
if (column.updateDate)
mode = "updateDate";
if (column.deleteDate)
mode = "deleteDate";
if (column.version)
mode = "version";
if (column.treeChildrenCount)
Expand Down
14 changes: 14 additions & 0 deletions src/error/MissingDeleteDateColumnError.ts
@@ -0,0 +1,14 @@
import {EntityMetadata} from "../metadata/EntityMetadata";

/**
*/
export class MissingDeleteDateColumnError extends Error {
name = "MissingDeleteDateColumnError";

constructor(entityMetadata: EntityMetadata) {
super();
Object.setPrototypeOf(this, MissingDeleteDateColumnError.prototype);
this.message = `Entity "${entityMetadata.name}" does not have delete date columns.`;
}

}