diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts index 77bd3216ae..da1c91c7eb 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts @@ -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: [ diff --git a/packages/twenty-server/src/modules/note/query-hooks/note-delete-many.post-query.hook.ts b/packages/twenty-server/src/modules/note/query-hooks/note-delete-many.post-query.hook.ts new file mode 100644 index 0000000000..177358cf04 --- /dev/null +++ b/packages/twenty-server/src/modules/note/query-hooks/note-delete-many.post-query.hook.ts @@ -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 { + await this.notePostQueryHookService.handleNoteTargetsDelete( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/note/query-hooks/note-delete-one.post-query.hook.ts b/packages/twenty-server/src/modules/note/query-hooks/note-delete-one.post-query.hook.ts new file mode 100644 index 0000000000..367915d804 --- /dev/null +++ b/packages/twenty-server/src/modules/note/query-hooks/note-delete-one.post-query.hook.ts @@ -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 { + await this.notePostQueryHookService.handleNoteTargetsDelete( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/note/query-hooks/note-post-query-hook.service.ts b/packages/twenty-server/src/modules/note/query-hooks/note-post-query-hook.service.ts new file mode 100644 index 0000000000..3f10408ba9 --- /dev/null +++ b/packages/twenty-server/src/modules/note/query-hooks/note-post-query-hook.service.ts @@ -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 { + 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( + workspace.id, + 'noteTarget', + ); + + await noteTargetRepository.softDelete({ + noteId: In(payload.map((note) => note.id)), + }); + }, + ); + } + + async handleNoteTargetsRestore( + authContext: AuthContext, + payload: NoteWorkspaceEntity[], + ): Promise { + 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( + workspace.id, + 'noteTarget', + ); + + await noteTargetRepository.restore({ + noteId: In(payload.map((note) => note.id)), + }); + }, + ); + } +} diff --git a/packages/twenty-server/src/modules/note/query-hooks/note-query-hook.module.ts b/packages/twenty-server/src/modules/note/query-hooks/note-query-hook.module.ts new file mode 100644 index 0000000000..5b028ee5e5 --- /dev/null +++ b/packages/twenty-server/src/modules/note/query-hooks/note-query-hook.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/modules/note/query-hooks/note-restore-many.post-query.hook.ts b/packages/twenty-server/src/modules/note/query-hooks/note-restore-many.post-query.hook.ts new file mode 100644 index 0000000000..fcc3324143 --- /dev/null +++ b/packages/twenty-server/src/modules/note/query-hooks/note-restore-many.post-query.hook.ts @@ -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 { + await this.notePostQueryHookService.handleNoteTargetsRestore( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/note/query-hooks/note-restore-one.post-query.hook.ts b/packages/twenty-server/src/modules/note/query-hooks/note-restore-one.post-query.hook.ts new file mode 100644 index 0000000000..644a84c6d4 --- /dev/null +++ b/packages/twenty-server/src/modules/note/query-hooks/note-restore-one.post-query.hook.ts @@ -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 { + await this.notePostQueryHookService.handleNoteTargetsRestore( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/task/query-hooks/task-delete-many.post-query.hook.ts b/packages/twenty-server/src/modules/task/query-hooks/task-delete-many.post-query.hook.ts new file mode 100644 index 0000000000..9f2fd4b87f --- /dev/null +++ b/packages/twenty-server/src/modules/task/query-hooks/task-delete-many.post-query.hook.ts @@ -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 { + await this.taskPostQueryHookService.handleTaskTargetsDelete( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/task/query-hooks/task-delete-one.post-query.hook.ts b/packages/twenty-server/src/modules/task/query-hooks/task-delete-one.post-query.hook.ts new file mode 100644 index 0000000000..f9e3a9bfb6 --- /dev/null +++ b/packages/twenty-server/src/modules/task/query-hooks/task-delete-one.post-query.hook.ts @@ -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 { + await this.taskPostQueryHookService.handleTaskTargetsDelete( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/task/query-hooks/task-post-query-hook.service.ts b/packages/twenty-server/src/modules/task/query-hooks/task-post-query-hook.service.ts new file mode 100644 index 0000000000..fc312b86c4 --- /dev/null +++ b/packages/twenty-server/src/modules/task/query-hooks/task-post-query-hook.service.ts @@ -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 { + 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( + workspace.id, + 'taskTarget', + ); + + await taskTargetRepository.softDelete({ + taskId: In(payload.map((task) => task.id)), + }); + }, + ); + } + + async handleTaskTargetsRestore( + authContext: AuthContext, + payload: TaskWorkspaceEntity[], + ): Promise { + 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( + workspace.id, + 'taskTarget', + ); + + await taskTargetRepository.restore({ + taskId: In(payload.map((task) => task.id)), + }); + }, + ); + } +} diff --git a/packages/twenty-server/src/modules/task/query-hooks/task-query-hook.module.ts b/packages/twenty-server/src/modules/task/query-hooks/task-query-hook.module.ts new file mode 100644 index 0000000000..c133de1c11 --- /dev/null +++ b/packages/twenty-server/src/modules/task/query-hooks/task-query-hook.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/modules/task/query-hooks/task-restore-many.post-query.hook.ts b/packages/twenty-server/src/modules/task/query-hooks/task-restore-many.post-query.hook.ts new file mode 100644 index 0000000000..766d4b6b43 --- /dev/null +++ b/packages/twenty-server/src/modules/task/query-hooks/task-restore-many.post-query.hook.ts @@ -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 { + await this.taskPostQueryHookService.handleTaskTargetsRestore( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/src/modules/task/query-hooks/task-restore-one.post-query.hook.ts b/packages/twenty-server/src/modules/task/query-hooks/task-restore-one.post-query.hook.ts new file mode 100644 index 0000000000..6e75c8ef10 --- /dev/null +++ b/packages/twenty-server/src/modules/task/query-hooks/task-restore-one.post-query.hook.ts @@ -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 { + await this.taskPostQueryHookService.handleTaskTargetsRestore( + authContext, + payload, + ); + } +} diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/notes-post-hook.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/notes-post-hook.integration-spec.ts new file mode 100644 index 0000000000..8f3c8d0853 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/notes-post-hook.integration-spec.ts @@ -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(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks-post-hook.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks-post-hook.integration-spec.ts new file mode 100644 index 0000000000..758535385e --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks-post-hook.integration-spec.ts @@ -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(); + }); +});