From 04dec08d045dbd6acc85a25d6e3f8ce7be7616f8 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 7 Aug 2025 11:20:32 +0100 Subject: [PATCH] fix(backend): delete follow relations for deleted users (#8805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * delete follows * migration-deleted-user-follows --------- Co-authored-by: Wolfgang Huß --- .../20250804140837-delted-user-follows.ts | 51 +++++++++++++++++++ backend/src/graphql/resolvers/users.spec.ts | 34 +++++++++++++ backend/src/graphql/resolvers/users.ts | 3 ++ 3 files changed, 88 insertions(+) create mode 100644 backend/src/db/migrations/20250804140837-delted-user-follows.ts diff --git a/backend/src/db/migrations/20250804140837-delted-user-follows.ts b/backend/src/db/migrations/20250804140837-delted-user-follows.ts new file mode 100644 index 000000000..76ccce9c6 --- /dev/null +++ b/backend/src/db/migrations/20250804140837-delted-user-follows.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' + +export const description = 'Delete follow relation for deleted users' + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (:User {deleted: true})-[follow:FOLLOWS]-(:User) + DELETE follow; + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // cannot be rolled back + // Implement your migration here. + // await transaction.run(``) + // await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} diff --git a/backend/src/graphql/resolvers/users.spec.ts b/backend/src/graphql/resolvers/users.spec.ts index e870c7cd6..87f947620 100644 --- a/backend/src/graphql/resolvers/users.spec.ts +++ b/backend/src/graphql/resolvers/users.spec.ts @@ -554,6 +554,40 @@ describe('Delete a User as admin', () => { await expect(database.neode.all('SocialMedia')).resolves.toHaveLength(0) }) }) + + describe('connected follow relations', () => { + beforeEach(async () => { + const userIFollow = await Factory.build('user', { + name: 'User I Follow', + about: 'I follow this user', + id: 'uifollow', + }) + + const userFollowingMe = await Factory.build('user', { + name: 'User Following Me', + about: 'This user follows me', + id: 'ufollowsme', + }) + await user.relateTo(userIFollow, 'following') + await userFollowingMe.relateTo(user, 'following') + }) + + it('will be removed completely', async () => { + const relation = await database.neode.cypher( + 'MATCH (user:User {id: $id})-[relationship:FOLLOWS]-(:User) RETURN relationship', + { id: (await user.toJson()).id }, + ) + const relations = relation.records.map((record) => record.get('relationship')) + expect(relations).toHaveLength(2) + await mutate({ mutation: deleteUserMutation, variables }) + const relation2 = await database.neode.cypher( + 'MATCH (user:User {id: $id})-[relationship:FOLLOWS]-(:User) RETURN relationship', + { id: (await user.toJson()).id }, + ) + const relations2 = relation2.records.map((record) => record.get('relationship')) + expect(relations2).toHaveLength(0) + }) + }) }) }) }) diff --git a/backend/src/graphql/resolvers/users.ts b/backend/src/graphql/resolvers/users.ts index ff512416b..096dfe521 100644 --- a/backend/src/graphql/resolvers/users.ts +++ b/backend/src/graphql/resolvers/users.ts @@ -280,6 +280,9 @@ export default { WITH user OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia) DETACH DELETE socialMedia + WITH user + OPTIONAL MATCH (user)-[follow:FOLLOWS]-(:User) + DELETE follow RETURN user {.*} `, { userId },