fix(16819): Add NoteDeleteOnePostQueryHook for soft removing note targets (#16826)
Fixes [#16819](https://github.com/twentyhq/twenty/issues/16819) Added a post-query hook (NoteDeleteOnePostQueryHook) that automatically soft-deletes all associated noteTarget records when a note is deleted. ## Key changes: Created `note-delete-one.post-query.hook.ts` Uses `GlobalWorkspaceOrmManager` to access workspace entities correctly Executes in the proper workspace context to ensure data isolation Soft-deletes noteTarget records to maintain referential consistency ## Testing 1. Company notes Create a Company Create and attach a Note to the Company Navigate to Company → Notes page (should display the note) Delete the Note Navigate to Company → Notes page (should no longer crash) Restore the Note (should appear again) Delete and destroy the Note permanently Navigate to Company → Notes page (should still work, just empty) 2. Opportunity notes Create an Opportunity Create and attach a Note to the Opportunity Navigate to Opportunity → Notes page (should display the note) Delete the Note Navigate to Opportunity → Notes page (should no longer crash) Restore the Note (should appear again) Delete and destroy the Note permanently Navigate to Opportunity → Notes page (should still work, just empty) ### Expected behavior: Company Notes page remains functional at all stages No console errors Deleted notes properly excluded from the view Other notes on the same Company remain accessible Additional Notes This fix ensures data consistency by maintaining the relationship between notes and their targets throughout the deletion lifecycle. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TaskPostQueryHookService } from 'src/modules/task/query-hooks/task-post-query-hook.service';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceQueryHook({
|
||||
key: `task.deleteMany`,
|
||||
type: WorkspaceQueryHookType.POST_HOOK,
|
||||
})
|
||||
export class TaskDeleteManyPostQueryHook
|
||||
implements WorkspacePostQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly taskPostQueryHookService: TaskPostQueryHookService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: TaskWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
await this.taskPostQueryHookService.handleTaskTargetsDelete(
|
||||
authContext,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TaskPostQueryHookService } from 'src/modules/task/query-hooks/task-post-query-hook.service';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceQueryHook({
|
||||
key: `task.deleteOne`,
|
||||
type: WorkspaceQueryHookType.POST_HOOK,
|
||||
})
|
||||
export class TaskDeleteOnePostQueryHook
|
||||
implements WorkspacePostQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly taskPostQueryHookService: TaskPostQueryHookService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: TaskWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
await this.taskPostQueryHookService.handleTaskTargetsDelete(
|
||||
authContext,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class TaskPostQueryHookService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
async handleTaskTargetsDelete(
|
||||
authContext: AuthContext,
|
||||
payload: TaskWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
if (!payload || payload?.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext as WorkspaceAuthContext,
|
||||
async () => {
|
||||
const taskTargetRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<TaskTargetWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'taskTarget',
|
||||
);
|
||||
|
||||
await taskTargetRepository.softDelete({
|
||||
taskId: In(payload.map((task) => task.id)),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async handleTaskTargetsRestore(
|
||||
authContext: AuthContext,
|
||||
payload: TaskWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
if (!payload || payload?.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext as WorkspaceAuthContext,
|
||||
async () => {
|
||||
const taskTargetRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<TaskTargetWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'taskTarget',
|
||||
);
|
||||
|
||||
await taskTargetRepository.restore({
|
||||
taskId: In(payload.map((task) => task.id)),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TaskDeleteManyPostQueryHook } from 'src/modules/task/query-hooks/task-delete-many.post-query.hook';
|
||||
import { TaskDeleteOnePostQueryHook } from 'src/modules/task/query-hooks/task-delete-one.post-query.hook';
|
||||
import { TaskPostQueryHookService } from 'src/modules/task/query-hooks/task-post-query-hook.service';
|
||||
import { TaskRestoreManyPostQueryHook } from 'src/modules/task/query-hooks/task-restore-many.post-query.hook';
|
||||
import { TaskRestoreOnePostQueryHook } from 'src/modules/task/query-hooks/task-restore-one.post-query.hook';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
TaskPostQueryHookService,
|
||||
TaskDeleteManyPostQueryHook,
|
||||
TaskDeleteOnePostQueryHook,
|
||||
TaskRestoreManyPostQueryHook,
|
||||
TaskRestoreOnePostQueryHook,
|
||||
],
|
||||
})
|
||||
export class TaskQueryHookModule {}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TaskPostQueryHookService } from 'src/modules/task/query-hooks/task-post-query-hook.service';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceQueryHook({
|
||||
key: `task.restoreMany`,
|
||||
type: WorkspaceQueryHookType.POST_HOOK,
|
||||
})
|
||||
export class TaskRestoreManyPostQueryHook
|
||||
implements WorkspacePostQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly taskPostQueryHookService: TaskPostQueryHookService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: TaskWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
await this.taskPostQueryHookService.handleTaskTargetsRestore(
|
||||
authContext,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TaskPostQueryHookService } from 'src/modules/task/query-hooks/task-post-query-hook.service';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceQueryHook({
|
||||
key: `task.restoreOne`,
|
||||
type: WorkspaceQueryHookType.POST_HOOK,
|
||||
})
|
||||
export class TaskRestoreOnePostQueryHook
|
||||
implements WorkspacePostQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly taskPostQueryHookService: TaskPostQueryHookService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: TaskWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
await this.taskPostQueryHookService.handleTaskTargetsRestore(
|
||||
authContext,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user