Skip to content

Commit

Permalink
feat: soft delete (#5034)
Browse files Browse the repository at this point in the history
* added @DeleteDateColumn

* updated test for embedded-with-special-columns

* added the softDelete and restore methods to QueryBuilder

* added test for query builder > soft-delete

* added the softDelete and restore methods to repository

* added test for repository > soft-delete and restore

* added the softRemove and recover methods to repository

* added test for repository > soft-remove and recover

* fixed the title of the test for repository > soft-delete

* added the support of the cascades soft-remove and recover

* added test: support of the cascades soft-remove and recover

* fixed test for should perform restory with limit correctly: missing applying the limit method

* fixed the wrong comment for recover operation

* added the global condition of non-deleted to query-builder for the entity with delete date columns

* added the global condition of non-deleted to repository for the entity with delete date columns

* updated test for the global condition of non-deleted

* added the test for soft-delete and restore properties inside embeds as well

* added test to query-builder for the global condition of non-deleted

* updated test to repository for the global condition of non-deleted

* added test to repository for the global condition of non-deleted

* fixed comment for the test 'find with the global condition of non-deleted and eager relation'

* fixed can't add the corrent global condition as the missing of aliasNamePrefix

* fixed can't get the correct result as the missing of the ordering by id

* fixed should use propertyName instead of databaseName

* added deleteDate and deleteDateNullable for sap
  • Loading branch information
iWinston committed Feb 25, 2020
1 parent f9505c9 commit 3227c0b
Show file tree
Hide file tree
Showing 63 changed files with 2,332 additions and 28 deletions.
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.`;
}

}

0 comments on commit 3227c0b

Please sign in to comment.