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:
Álvaro Fernández
2026-01-07 09:30:08 +01:00
committed by GitHub
parent 5f4b6e7ea4
commit e4f8d804d5
15 changed files with 1102 additions and 0 deletions
@@ -10,6 +10,8 @@ import { CalendarQueryHookModule } from 'src/modules/calendar/common/query-hooks
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
import { DashboardQueryHookModule } from 'src/modules/dashboard/query-hooks/dashboard-query-hook.module';
import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module';
import { NoteQueryHookModule } from 'src/modules/note/query-hooks/note-query-hook.module';
import { TaskQueryHookModule } from 'src/modules/task/query-hooks/task-query-hook.module';
import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module';
@Module({
@@ -20,6 +22,8 @@ import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/que
DashboardQueryHookModule,
BlocklistQueryHookModule,
WorkspaceMemberQueryHookModule,
NoteQueryHookModule,
TaskQueryHookModule,
DiscoveryModule,
],
providers: [
@@ -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 { NotePostQueryHookService } from 'src/modules/note/query-hooks/note-post-query-hook.service';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
@Injectable()
@WorkspaceQueryHook({
key: `note.deleteMany`,
type: WorkspaceQueryHookType.POST_HOOK,
})
export class NoteDeleteManyPostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly notePostQueryHookService: NotePostQueryHookService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: NoteWorkspaceEntity[],
): Promise<void> {
await this.notePostQueryHookService.handleNoteTargetsDelete(
authContext,
payload,
);
}
}
@@ -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 { NotePostQueryHookService } from 'src/modules/note/query-hooks/note-post-query-hook.service';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
@Injectable()
@WorkspaceQueryHook({
key: `note.deleteOne`,
type: WorkspaceQueryHookType.POST_HOOK,
})
export class NoteDeleteOnePostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly notePostQueryHookService: NotePostQueryHookService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: NoteWorkspaceEntity[],
): Promise<void> {
await this.notePostQueryHookService.handleNoteTargetsDelete(
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 { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
@Injectable()
export class NotePostQueryHookService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async handleNoteTargetsDelete(
authContext: AuthContext,
payload: NoteWorkspaceEntity[],
): Promise<void> {
if (!payload || payload?.length === 0) {
return;
}
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext as WorkspaceAuthContext,
async () => {
const noteTargetRepository =
await this.globalWorkspaceOrmManager.getRepository<NoteTargetWorkspaceEntity>(
workspace.id,
'noteTarget',
);
await noteTargetRepository.softDelete({
noteId: In(payload.map((note) => note.id)),
});
},
);
}
async handleNoteTargetsRestore(
authContext: AuthContext,
payload: NoteWorkspaceEntity[],
): Promise<void> {
if (!payload || payload?.length === 0) {
return;
}
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext as WorkspaceAuthContext,
async () => {
const noteTargetRepository =
await this.globalWorkspaceOrmManager.getRepository<NoteTargetWorkspaceEntity>(
workspace.id,
'noteTarget',
);
await noteTargetRepository.restore({
noteId: In(payload.map((note) => note.id)),
});
},
);
}
}
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { NoteDeleteManyPostQueryHook } from 'src/modules/note/query-hooks/note-delete-many.post-query.hook';
import { NoteDeleteOnePostQueryHook } from 'src/modules/note/query-hooks/note-delete-one.post-query.hook';
import { NotePostQueryHookService } from 'src/modules/note/query-hooks/note-post-query-hook.service';
import { NoteRestoreManyPostQueryHook } from 'src/modules/note/query-hooks/note-restore-many.post-query.hook';
import { NoteRestoreOnePostQueryHook } from 'src/modules/note/query-hooks/note-restore-one.post-query.hook';
@Module({
providers: [
NotePostQueryHookService,
NoteDeleteManyPostQueryHook,
NoteDeleteOnePostQueryHook,
NoteRestoreManyPostQueryHook,
NoteRestoreOnePostQueryHook,
],
})
export class NoteQueryHookModule {}
@@ -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 { NotePostQueryHookService } from 'src/modules/note/query-hooks/note-post-query-hook.service';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
@Injectable()
@WorkspaceQueryHook({
key: `note.restoreMany`,
type: WorkspaceQueryHookType.POST_HOOK,
})
export class NoteRestoreManyPostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly notePostQueryHookService: NotePostQueryHookService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: NoteWorkspaceEntity[],
): Promise<void> {
await this.notePostQueryHookService.handleNoteTargetsRestore(
authContext,
payload,
);
}
}
@@ -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 { NotePostQueryHookService } from 'src/modules/note/query-hooks/note-post-query-hook.service';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
@Injectable()
@WorkspaceQueryHook({
key: `note.restoreOne`,
type: WorkspaceQueryHookType.POST_HOOK,
})
export class NoteRestoreOnePostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly notePostQueryHookService: NotePostQueryHookService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: NoteWorkspaceEntity[],
): Promise<void> {
await this.notePostQueryHookService.handleNoteTargetsRestore(
authContext,
payload,
);
}
}
@@ -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,
);
}
}
@@ -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 {}
@@ -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,
);
}
}
@@ -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,
);
}
}
@@ -0,0 +1,324 @@
import { randomUUID } from 'crypto';
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util';
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { restoreManyOperationFactory } from 'test/integration/graphql/utils/restore-many-operation-factory.util';
import { restoreOneOperationFactory } from 'test/integration/graphql/utils/restore-one-operation-factory.util';
const NOTE_GQL_FIELDS = `
id
title
deletedAt
`;
const NOTE_TARGET_GQL_FIELDS = `
id
noteId
deletedAt
`;
describe('Note post-query hooks', () => {
const noteIds: string[] = [];
const noteTargetIds: string[] = [];
afterAll(async () => {
if (noteIds.length > 0) {
const destroyNotesOperation = destroyManyOperationFactory({
objectMetadataSingularName: 'note',
objectMetadataPluralName: 'notes',
gqlFields: 'id',
filter: { id: { in: noteIds } },
});
await makeGraphqlAPIRequest(destroyNotesOperation);
}
if (noteTargetIds.length > 0) {
const destroyNoteTargetsOperation = destroyManyOperationFactory({
objectMetadataSingularName: 'noteTarget',
objectMetadataPluralName: 'noteTargets',
gqlFields: 'id',
filter: { id: { in: noteTargetIds } },
});
await makeGraphqlAPIRequest(destroyNoteTargetsOperation);
}
});
it('deleteOne should soft delete related noteTargets', async () => {
const noteId = randomUUID();
const noteTargetId = randomUUID();
noteIds.push(noteId);
noteTargetIds.push(noteTargetId);
const createNoteOperation = createOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
data: { id: noteId, title: 'Test Note for DeleteOne' },
});
await makeGraphqlAPIRequest(createNoteOperation);
const createNoteTargetOperation = createOneOperationFactory({
objectMetadataSingularName: 'noteTarget',
gqlFields: NOTE_TARGET_GQL_FIELDS,
data: { id: noteTargetId, noteId },
});
await makeGraphqlAPIRequest(createNoteTargetOperation);
const deleteNoteOperation = deleteOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
recordId: noteId,
});
const deleteResponse = await makeGraphqlAPIRequest(deleteNoteOperation);
expect(deleteResponse.body.data.deleteNote).toBeDefined();
expect(deleteResponse.body.data.deleteNote.deletedAt).not.toBeNull();
const findNoteTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'noteTarget',
objectMetadataPluralName: 'noteTargets',
gqlFields: NOTE_TARGET_GQL_FIELDS,
filter: {
id: { eq: noteTargetId },
not: { deletedAt: { is: 'NULL' } },
},
});
const noteTargetResponse = await makeGraphqlAPIRequest(
findNoteTargetsOperation,
);
expect(noteTargetResponse.body.data.noteTargets.edges).toHaveLength(1);
expect(
noteTargetResponse.body.data.noteTargets.edges[0].node.deletedAt,
).not.toBeNull();
});
it('deleteMany should soft delete related noteTargets', async () => {
const noteId1 = randomUUID();
const noteId2 = randomUUID();
const noteTargetId1 = randomUUID();
const noteTargetId2 = randomUUID();
noteIds.push(noteId1, noteId2);
noteTargetIds.push(noteTargetId1, noteTargetId2);
const createNote1Operation = createOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
data: { id: noteId1, title: 'Test Note 1 for DeleteMany' },
});
const createNote2Operation = createOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
data: { id: noteId2, title: 'Test Note 2 for DeleteMany' },
});
await Promise.all([
makeGraphqlAPIRequest(createNote1Operation),
makeGraphqlAPIRequest(createNote2Operation),
]);
const createNoteTarget1Operation = createOneOperationFactory({
objectMetadataSingularName: 'noteTarget',
gqlFields: NOTE_TARGET_GQL_FIELDS,
data: { id: noteTargetId1, noteId: noteId1 },
});
const createNoteTarget2Operation = createOneOperationFactory({
objectMetadataSingularName: 'noteTarget',
gqlFields: NOTE_TARGET_GQL_FIELDS,
data: { id: noteTargetId2, noteId: noteId2 },
});
await Promise.all([
makeGraphqlAPIRequest(createNoteTarget1Operation),
makeGraphqlAPIRequest(createNoteTarget2Operation),
]);
const deleteNotesOperation = deleteManyOperationFactory({
objectMetadataSingularName: 'note',
objectMetadataPluralName: 'notes',
gqlFields: NOTE_GQL_FIELDS,
filter: { id: { in: [noteId1, noteId2] } },
});
const deleteResponse = await makeGraphqlAPIRequest(deleteNotesOperation);
expect(deleteResponse.body.data.deleteNotes).toHaveLength(2);
const findNoteTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'noteTarget',
objectMetadataPluralName: 'noteTargets',
gqlFields: NOTE_TARGET_GQL_FIELDS,
filter: {
id: { in: [noteTargetId1, noteTargetId2] },
not: { deletedAt: { is: 'NULL' } },
},
});
const noteTargetResponse = await makeGraphqlAPIRequest(
findNoteTargetsOperation,
);
expect(noteTargetResponse.body.data.noteTargets.edges).toHaveLength(2);
expect(
noteTargetResponse.body.data.noteTargets.edges[0].node.deletedAt,
).not.toBeNull();
expect(
noteTargetResponse.body.data.noteTargets.edges[1].node.deletedAt,
).not.toBeNull();
});
it('restoreOne should restore related noteTargets', async () => {
const noteId = randomUUID();
const noteTargetId = randomUUID();
noteIds.push(noteId);
noteTargetIds.push(noteTargetId);
const createNoteOperation = createOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
data: { id: noteId, title: 'Test Note for RestoreOne' },
});
await makeGraphqlAPIRequest(createNoteOperation);
const createNoteTargetOperation = createOneOperationFactory({
objectMetadataSingularName: 'noteTarget',
gqlFields: NOTE_TARGET_GQL_FIELDS,
data: { id: noteTargetId, noteId },
});
await makeGraphqlAPIRequest(createNoteTargetOperation);
const deleteNoteOperation = deleteOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
recordId: noteId,
});
await makeGraphqlAPIRequest(deleteNoteOperation);
const restoreNoteOperation = restoreOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
recordId: noteId,
});
const restoreResponse = await makeGraphqlAPIRequest(restoreNoteOperation);
expect(restoreResponse.body.data.restoreNote).toBeDefined();
expect(restoreResponse.body.data.restoreNote.deletedAt).toBeNull();
const findNoteTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'noteTarget',
objectMetadataPluralName: 'noteTargets',
gqlFields: NOTE_TARGET_GQL_FIELDS,
filter: { id: { eq: noteTargetId } },
});
const noteTargetResponse = await makeGraphqlAPIRequest(
findNoteTargetsOperation,
);
expect(noteTargetResponse.body.data.noteTargets.edges).toHaveLength(1);
expect(
noteTargetResponse.body.data.noteTargets.edges[0].node.deletedAt,
).toBeNull();
});
it('restoreMany should restore related noteTargets', async () => {
const noteId1 = randomUUID();
const noteId2 = randomUUID();
const noteTargetId1 = randomUUID();
const noteTargetId2 = randomUUID();
noteIds.push(noteId1, noteId2);
noteTargetIds.push(noteTargetId1, noteTargetId2);
const createNote1Operation = createOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
data: { id: noteId1, title: 'Test Note 1 for RestoreMany' },
});
const createNote2Operation = createOneOperationFactory({
objectMetadataSingularName: 'note',
gqlFields: NOTE_GQL_FIELDS,
data: { id: noteId2, title: 'Test Note 2 for RestoreMany' },
});
await Promise.all([
makeGraphqlAPIRequest(createNote1Operation),
makeGraphqlAPIRequest(createNote2Operation),
]);
const createNoteTarget1Operation = createOneOperationFactory({
objectMetadataSingularName: 'noteTarget',
gqlFields: NOTE_TARGET_GQL_FIELDS,
data: { id: noteTargetId1, noteId: noteId1 },
});
const createNoteTarget2Operation = createOneOperationFactory({
objectMetadataSingularName: 'noteTarget',
gqlFields: NOTE_TARGET_GQL_FIELDS,
data: { id: noteTargetId2, noteId: noteId2 },
});
await Promise.all([
makeGraphqlAPIRequest(createNoteTarget1Operation),
makeGraphqlAPIRequest(createNoteTarget2Operation),
]);
const deleteNotesOperation = deleteManyOperationFactory({
objectMetadataSingularName: 'note',
objectMetadataPluralName: 'notes',
gqlFields: NOTE_GQL_FIELDS,
filter: { id: { in: [noteId1, noteId2] } },
});
await makeGraphqlAPIRequest(deleteNotesOperation);
const restoreNotesOperation = restoreManyOperationFactory({
objectMetadataSingularName: 'note',
objectMetadataPluralName: 'notes',
gqlFields: NOTE_GQL_FIELDS,
filter: { id: { in: [noteId1, noteId2] } },
});
const restoreResponse = await makeGraphqlAPIRequest(restoreNotesOperation);
expect(restoreResponse.body.data.restoreNotes).toHaveLength(2);
const findNoteTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'noteTarget',
objectMetadataPluralName: 'noteTargets',
gqlFields: NOTE_TARGET_GQL_FIELDS,
filter: { id: { in: [noteTargetId1, noteTargetId2] } },
});
const noteTargetResponse = await makeGraphqlAPIRequest(
findNoteTargetsOperation,
);
expect(noteTargetResponse.body.data.noteTargets.edges).toHaveLength(2);
expect(
noteTargetResponse.body.data.noteTargets.edges[0].node.deletedAt,
).toBeNull();
expect(
noteTargetResponse.body.data.noteTargets.edges[1].node.deletedAt,
).toBeNull();
});
});
@@ -0,0 +1,324 @@
import { randomUUID } from 'crypto';
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util';
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { restoreManyOperationFactory } from 'test/integration/graphql/utils/restore-many-operation-factory.util';
import { restoreOneOperationFactory } from 'test/integration/graphql/utils/restore-one-operation-factory.util';
const TASK_GQL_FIELDS = `
id
title
deletedAt
`;
const TASK_TARGET_GQL_FIELDS = `
id
taskId
deletedAt
`;
describe('Task post-query hooks', () => {
const taskIds: string[] = [];
const taskTargetIds: string[] = [];
afterAll(async () => {
if (taskIds.length > 0) {
const destroyTasksOperation = destroyManyOperationFactory({
objectMetadataSingularName: 'task',
objectMetadataPluralName: 'tasks',
gqlFields: 'id',
filter: { id: { in: taskIds } },
});
await makeGraphqlAPIRequest(destroyTasksOperation);
}
if (taskTargetIds.length > 0) {
const destroyTaskTargetsOperation = destroyManyOperationFactory({
objectMetadataSingularName: 'taskTarget',
objectMetadataPluralName: 'taskTargets',
gqlFields: 'id',
filter: { id: { in: taskTargetIds } },
});
await makeGraphqlAPIRequest(destroyTaskTargetsOperation);
}
});
it('deleteOne should soft delete related taskTargets', async () => {
const taskId = randomUUID();
const taskTargetId = randomUUID();
taskIds.push(taskId);
taskTargetIds.push(taskTargetId);
const createTaskOperation = createOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
data: { id: taskId, title: 'Test Task for DeleteOne' },
});
await makeGraphqlAPIRequest(createTaskOperation);
const createTaskTargetOperation = createOneOperationFactory({
objectMetadataSingularName: 'taskTarget',
gqlFields: TASK_TARGET_GQL_FIELDS,
data: { id: taskTargetId, taskId },
});
await makeGraphqlAPIRequest(createTaskTargetOperation);
const deleteTaskOperation = deleteOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
recordId: taskId,
});
const deleteResponse = await makeGraphqlAPIRequest(deleteTaskOperation);
expect(deleteResponse.body.data.deleteTask).toBeDefined();
expect(deleteResponse.body.data.deleteTask.deletedAt).not.toBeNull();
const findTaskTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'taskTarget',
objectMetadataPluralName: 'taskTargets',
gqlFields: TASK_TARGET_GQL_FIELDS,
filter: {
id: { eq: taskTargetId },
not: { deletedAt: { is: 'NULL' } },
},
});
const taskTargetResponse = await makeGraphqlAPIRequest(
findTaskTargetsOperation,
);
expect(taskTargetResponse.body.data.taskTargets.edges).toHaveLength(1);
expect(
taskTargetResponse.body.data.taskTargets.edges[0].node.deletedAt,
).not.toBeNull();
});
it('deleteMany should soft delete related taskTargets', async () => {
const taskId1 = randomUUID();
const taskId2 = randomUUID();
const taskTargetId1 = randomUUID();
const taskTargetId2 = randomUUID();
taskIds.push(taskId1, taskId2);
taskTargetIds.push(taskTargetId1, taskTargetId2);
const createTask1Operation = createOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
data: { id: taskId1, title: 'Test Task 1 for DeleteMany' },
});
const createTask2Operation = createOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
data: { id: taskId2, title: 'Test Task 2 for DeleteMany' },
});
await Promise.all([
makeGraphqlAPIRequest(createTask1Operation),
makeGraphqlAPIRequest(createTask2Operation),
]);
const createTaskTarget1Operation = createOneOperationFactory({
objectMetadataSingularName: 'taskTarget',
gqlFields: TASK_TARGET_GQL_FIELDS,
data: { id: taskTargetId1, taskId: taskId1 },
});
const createTaskTarget2Operation = createOneOperationFactory({
objectMetadataSingularName: 'taskTarget',
gqlFields: TASK_TARGET_GQL_FIELDS,
data: { id: taskTargetId2, taskId: taskId2 },
});
await Promise.all([
makeGraphqlAPIRequest(createTaskTarget1Operation),
makeGraphqlAPIRequest(createTaskTarget2Operation),
]);
const deleteTasksOperation = deleteManyOperationFactory({
objectMetadataSingularName: 'task',
objectMetadataPluralName: 'tasks',
gqlFields: TASK_GQL_FIELDS,
filter: { id: { in: [taskId1, taskId2] } },
});
const deleteResponse = await makeGraphqlAPIRequest(deleteTasksOperation);
expect(deleteResponse.body.data.deleteTasks).toHaveLength(2);
const findTaskTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'taskTarget',
objectMetadataPluralName: 'taskTargets',
gqlFields: TASK_TARGET_GQL_FIELDS,
filter: {
id: { in: [taskTargetId1, taskTargetId2] },
not: { deletedAt: { is: 'NULL' } },
},
});
const taskTargetResponse = await makeGraphqlAPIRequest(
findTaskTargetsOperation,
);
expect(taskTargetResponse.body.data.taskTargets.edges).toHaveLength(2);
expect(
taskTargetResponse.body.data.taskTargets.edges[0].node.deletedAt,
).not.toBeNull();
expect(
taskTargetResponse.body.data.taskTargets.edges[1].node.deletedAt,
).not.toBeNull();
});
it('restoreOne should restore related taskTargets', async () => {
const taskId = randomUUID();
const taskTargetId = randomUUID();
taskIds.push(taskId);
taskTargetIds.push(taskTargetId);
const createTaskOperation = createOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
data: { id: taskId, title: 'Test Task for RestoreOne' },
});
await makeGraphqlAPIRequest(createTaskOperation);
const createTaskTargetOperation = createOneOperationFactory({
objectMetadataSingularName: 'taskTarget',
gqlFields: TASK_TARGET_GQL_FIELDS,
data: { id: taskTargetId, taskId },
});
await makeGraphqlAPIRequest(createTaskTargetOperation);
const deleteTaskOperation = deleteOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
recordId: taskId,
});
await makeGraphqlAPIRequest(deleteTaskOperation);
const restoreTaskOperation = restoreOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
recordId: taskId,
});
const restoreResponse = await makeGraphqlAPIRequest(restoreTaskOperation);
expect(restoreResponse.body.data.restoreTask).toBeDefined();
expect(restoreResponse.body.data.restoreTask.deletedAt).toBeNull();
const findTaskTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'taskTarget',
objectMetadataPluralName: 'taskTargets',
gqlFields: TASK_TARGET_GQL_FIELDS,
filter: { id: { eq: taskTargetId } },
});
const taskTargetResponse = await makeGraphqlAPIRequest(
findTaskTargetsOperation,
);
expect(taskTargetResponse.body.data.taskTargets.edges).toHaveLength(1);
expect(
taskTargetResponse.body.data.taskTargets.edges[0].node.deletedAt,
).toBeNull();
});
it('restoreMany should restore related taskTargets', async () => {
const taskId1 = randomUUID();
const taskId2 = randomUUID();
const taskTargetId1 = randomUUID();
const taskTargetId2 = randomUUID();
taskIds.push(taskId1, taskId2);
taskTargetIds.push(taskTargetId1, taskTargetId2);
const createTask1Operation = createOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
data: { id: taskId1, title: 'Test Task 1 for RestoreMany' },
});
const createTask2Operation = createOneOperationFactory({
objectMetadataSingularName: 'task',
gqlFields: TASK_GQL_FIELDS,
data: { id: taskId2, title: 'Test Task 2 for RestoreMany' },
});
await Promise.all([
makeGraphqlAPIRequest(createTask1Operation),
makeGraphqlAPIRequest(createTask2Operation),
]);
const createTaskTarget1Operation = createOneOperationFactory({
objectMetadataSingularName: 'taskTarget',
gqlFields: TASK_TARGET_GQL_FIELDS,
data: { id: taskTargetId1, taskId: taskId1 },
});
const createTaskTarget2Operation = createOneOperationFactory({
objectMetadataSingularName: 'taskTarget',
gqlFields: TASK_TARGET_GQL_FIELDS,
data: { id: taskTargetId2, taskId: taskId2 },
});
await Promise.all([
makeGraphqlAPIRequest(createTaskTarget1Operation),
makeGraphqlAPIRequest(createTaskTarget2Operation),
]);
const deleteTasksOperation = deleteManyOperationFactory({
objectMetadataSingularName: 'task',
objectMetadataPluralName: 'tasks',
gqlFields: TASK_GQL_FIELDS,
filter: { id: { in: [taskId1, taskId2] } },
});
await makeGraphqlAPIRequest(deleteTasksOperation);
const restoreTasksOperation = restoreManyOperationFactory({
objectMetadataSingularName: 'task',
objectMetadataPluralName: 'tasks',
gqlFields: TASK_GQL_FIELDS,
filter: { id: { in: [taskId1, taskId2] } },
});
const restoreResponse = await makeGraphqlAPIRequest(restoreTasksOperation);
expect(restoreResponse.body.data.restoreTasks).toHaveLength(2);
const findTaskTargetsOperation = findManyOperationFactory({
objectMetadataSingularName: 'taskTarget',
objectMetadataPluralName: 'taskTargets',
gqlFields: TASK_TARGET_GQL_FIELDS,
filter: { id: { in: [taskTargetId1, taskTargetId2] } },
});
const taskTargetResponse = await makeGraphqlAPIRequest(
findTaskTargetsOperation,
);
expect(taskTargetResponse.body.data.taskTargets.edges).toHaveLength(2);
expect(
taskTargetResponse.body.data.taskTargets.edges[0].node.deletedAt,
).toBeNull();
expect(
taskTargetResponse.body.data.taskTargets.edges[1].node.deletedAt,
).toBeNull();
});
});