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

fix: resolve issue delete column null on after update event subscriber #8227

Closed
wants to merge 8 commits into from
4 changes: 2 additions & 2 deletions src/persistence/SubjectExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,11 +735,11 @@ export class SubjectExecutor {
this.updateSpecialColumnsInInsertedAndUpdatedEntities(this.updateSubjects);

// update soft-removed entity properties
if (this.updateSubjects.length)
if (this.softRemoveSubjects.length)
this.updateSpecialColumnsInInsertedAndUpdatedEntities(this.softRemoveSubjects);

// update recovered entity properties
if (this.updateSubjects.length)
if (this.recoverSubjects.length)
this.updateSpecialColumnsInInsertedAndUpdatedEntities(this.recoverSubjects);

// remove ids from the entities that were removed
Expand Down
68 changes: 66 additions & 2 deletions src/query-builder/ReturningResultsEntityUpdator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class ReturningResultsEntityUpdator {
} else {

// for driver which do not support returning/output statement we need to perform separate query and load what we need
const updationColumns = this.getUpdationReturningColumns();
const updationColumns = this.expressionMap.extraReturningColumns;
if (updationColumns.length > 0) {

// get entity id by which we will get needed data
Expand All @@ -62,9 +62,10 @@ export class ReturningResultsEntityUpdator {
const loadedReturningColumns = await this.queryRunner.manager
.createQueryBuilder()
.select(metadata.primaryColumns.map(column => metadata.targetName + "." + column.propertyPath))
.addSelect(this.getUpdationReturningColumns().map(column => metadata.targetName + "." + column.propertyPath))
.addSelect(updationColumns.map(column => metadata.targetName + "." + column.propertyPath))
.from(metadata.target, metadata.targetName)
.where(entityId)
.withDeleted()
.setOption("create-pojo") // use POJO because created object can contain default values, e.g. property = null and those properties maight be overridden by merge process
.getOne() as any;

Expand Down Expand Up @@ -165,6 +166,60 @@ export class ReturningResultsEntityUpdator {
});
}

/**
* Updates entities with a special columns after soft delete and restore query execution.
*/
async softDelete(updateResult: UpdateResult, entities: ObjectLiteral[]): Promise<void> {
const metadata = this.expressionMap.mainAlias!.metadata;

await Promise.all(entities.map(async (entity, entityIndex) => {

// if database supports returning/output statement then we already should have updating values in the raw data returned by insert query
if (this.queryRunner.connection.driver.isReturningSqlSupported()) {
if (this.queryRunner.connection.driver instanceof OracleDriver && Array.isArray(updateResult.raw) && this.expressionMap.extraReturningColumns.length > 0) {
updateResult.raw = updateResult.raw.reduce((newRaw, rawItem, rawItemIndex) => {
newRaw[this.expressionMap.extraReturningColumns[rawItemIndex].databaseName] = rawItem[0];
return newRaw;
}, {} as ObjectLiteral);
}
const result = Array.isArray(updateResult.raw) ? updateResult.raw[entityIndex] : updateResult.raw;
const returningColumns = this.queryRunner.connection.driver.createGeneratedMap(metadata, result);
if (returningColumns) {
this.queryRunner.manager.merge(metadata.target as any, entity, returningColumns);
updateResult.generatedMaps.push(returningColumns);
}

} else {

// for driver which do not support returning/output statement we need to perform separate query and load what we need
const updationColumns = this.getSoftDeletionReturningColumns();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line of code the only difference with update method? If so, I would like to re-use update method to prevent another peace of complex code to be added into the repo... We can make update method to accept returning columns if needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh another diff I see is withDeleted. Maybe we can add a third parameter to update, like type: "default" | "soft-delete". Dunno, better to came up with better names, but having single update method is better for sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Got it.
I think withDeleted should be used with updates too. So if the entity is changed when soft deleted, it will fire afterUpdate event. It will not if there is no withDeleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Let me think in writing ))) Just helps me to think))) Those are only my thoughts. You can skip it.
To get rid of softDelete method we do this:
As this.expressionMap passed by reference in SoftDeleteQueryBulder.execute() and UpdateQueryBuilder.execute() methods and this.expressionMap.extraReturningColumns are the columns we need to update

// if update entity mode is enabled we may need extra columns for the returning statement
const returningResultsEntityUpdator = new ReturningResultsEntityUpdator(queryRunner, this.expressionMap);
if (this.expressionMap.updateEntity === true &&
    this.expressionMap.mainAlias!.hasMetadata &&
    this.expressionMap.whereEntities.length > 0) {
    this.expressionMap.extraReturningColumns = returningResultsEntityUpdator.getUpdationReturningColumns();
}

and expressionMap is required in the ReturningResultsEntityUpdator constructor

constructor(protected queryRunner: QueryRunner,
            protected expressionMap: QueryExpressionMap) {
}

and extraReturningColumns are never null or undefined

/**
 * Extra returning columns to be added to the returning statement if driver supports it.
 */
extraReturningColumns: ColumnMetadata[] = [];

we don't need to get updationColumns like so

const updationColumns = this.getSoftDeletionReturningColumns();

insead we can use

const updationColumns = this.expressionMap.extraReturningColumns;

So that updationColumns are unique for update and soft-delete and we don't need to call the same method multiple times.

Also, we can use withDeleted() for both update and soft delete as select is done by id. Guess it won't slow down a lot even with lots of soft-deleted rows. I already mentioned, that if the entity is changed while soft-deleted it won't make changes to the entity fired to afterUpdate event. And we can lose the blame in history if the event is used for that case.

if (updationColumns.length > 0) {

// get entity id by which we will get needed data
const entityId = this.expressionMap.mainAlias!.metadata.getEntityIdMap(entity);
if (!entityId)
throw new TypeORMError(`Cannot update entity because entity id is not set in the entity.`);

// execute query to get needed data
const loadedReturningColumns = await this.queryRunner.manager
.createQueryBuilder()
.select(metadata.primaryColumns.map(column => metadata.targetName + "." + column.propertyPath))
.addSelect(this.getSoftDeletionReturningColumns().map(column => metadata.targetName + "." + column.propertyPath))
.from(metadata.target, metadata.targetName)
.where(entityId)
.withDeleted()
.setOption("create-pojo") // use POJO because created object can contain default values, e.g. property = null and those properties maight be overridden by merge process
.getOne() as any;

if (loadedReturningColumns) {
this.queryRunner.manager.merge(metadata.target as any, entity, loadedReturningColumns);
updateResult.generatedMaps.push(loadedReturningColumns);
}
}
}
}));
}

/**
* Columns we need to be returned from the database when we insert entity.
*/
Expand Down Expand Up @@ -194,4 +249,13 @@ export class ReturningResultsEntityUpdator {
});
}

/**
* Columns we need to be returned from the database when we soft delete and restore entity.
*/
getSoftDeletionReturningColumns(): ColumnMetadata[] {
return this.expressionMap.mainAlias!.metadata.columns.filter(column => {
return column.isUpdateDate || column.isVersion || column.isDeleteDate;
});
}

}
4 changes: 2 additions & 2 deletions src/query-builder/SoftDeleteQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class SoftDeleteQueryBuilder<Entity> extends QueryBuilder<Entity> impleme
if (this.expressionMap.updateEntity === true &&
this.expressionMap.mainAlias!.hasMetadata &&
this.expressionMap.whereEntities.length > 0) {
this.expressionMap.extraReturningColumns = returningResultsEntityUpdator.getUpdationReturningColumns();
this.expressionMap.extraReturningColumns = returningResultsEntityUpdator.getSoftDeletionReturningColumns();
}

// execute update query
Expand All @@ -86,7 +86,7 @@ export class SoftDeleteQueryBuilder<Entity> extends QueryBuilder<Entity> impleme
if (this.expressionMap.updateEntity === true &&
this.expressionMap.mainAlias!.hasMetadata &&
this.expressionMap.whereEntities.length > 0) {
await returningResultsEntityUpdator.update(updateResult, this.expressionMap.whereEntities);
await returningResultsEntityUpdator.softDelete(updateResult, this.expressionMap.whereEntities);
}

// call after updation methods in listeners and subscribers
Expand Down
13 changes: 13 additions & 0 deletions test/github-issues/6327/entity/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from "../../../../src";

@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;

@UpdateDateColumn()
updatedAt: Date;

@DeleteDateColumn()
deletedAt: Date;
}
31 changes: 31 additions & 0 deletions test/github-issues/6327/issue-6327.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import "reflect-metadata";
import { createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
import { Connection } from "../../../src/connection/Connection";
import { Post } from "./entity/Post";

describe("github issues > #6327 softRemove DeleteDateColumn is null at Susbscriber's AfterUpdate method", () => {

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
subscribers: [__dirname + "/subscriber/*{.js,.ts}"],
schemaCreate: true,
dropSchema: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should send correct update and delete date columns to after update subscriber", () => Promise.all(connections.map(async connection => {

const manager = connection.manager;

const entity = new Post();
await manager.save(entity);

const deletedEntity = await manager.softRemove(entity, { data: { action: "soft-delete" } });

await manager.recover(deletedEntity, { data: { action: "restore" } });

})));

});
24 changes: 24 additions & 0 deletions test/github-issues/6327/subscriber/PostSubscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from "chai";
import { EntitySubscriberInterface, EventSubscriber, UpdateEvent } from "../../../../src";
import { Post } from "../entity/Post";

@EventSubscriber()
export class PostSubscriber implements EntitySubscriberInterface<Post> {
listenTo() {
return Post;
}

afterUpdate(event: UpdateEvent<Post>): void {
const { entity, queryRunner: { data } } = event;

expect(["soft-delete", "restore"]).to.include(data!.action);

if (data!.action === "soft-delete") {
expect(Object.prototype.toString.call(entity!.deletedAt)).to.be.eq("[object Date]");
}

if (data!.action === "restore") {
expect(entity!.deletedAt).to.be.null;
}
}
}