e4f8d804d5
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>
325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|