diff --git a/backend/src/graphql/resolvers/badges.ts b/backend/src/graphql/resolvers/badges.ts index 9c147ab3c..7eecd81d9 100644 --- a/backend/src/graphql/resolvers/badges.ts +++ b/backend/src/graphql/resolvers/badges.ts @@ -165,7 +165,7 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/comments.ts b/backend/src/graphql/resolvers/comments.ts index e07c6791d..d7e6141db 100644 --- a/backend/src/graphql/resolvers/comments.ts +++ b/backend/src/graphql/resolvers/comments.ts @@ -48,7 +48,7 @@ export default { const [comment] = await writeTxResultPromise return comment } finally { - session.close() + await session.close() } }, UpdateComment: async (_parent, params, context, _resolveInfo) => { @@ -71,7 +71,7 @@ export default { const [comment] = await writeTxResultPromise return comment } finally { - session.close() + await session.close() } }, DeleteComment: async (_parent, args, context, _resolveInfo) => { @@ -95,7 +95,7 @@ export default { const [comment] = await writeTxResultPromise return comment } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/donations.ts b/backend/src/graphql/resolvers/donations.ts index 017a97f5f..8acb2b062 100644 --- a/backend/src/graphql/resolvers/donations.ts +++ b/backend/src/graphql/resolvers/donations.ts @@ -6,59 +6,51 @@ export default { Query: { Donations: async (_parent, _params, context, _resolveInfo) => { const { driver } = context - let donations const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async (txc) => { - const donationsTransactionResponse = await txc.run( - ` - MATCH (donations:Donations) - WITH donations LIMIT 1 - RETURN donations - `, - {}, - ) - return donationsTransactionResponse.records.map( - (record) => record.get('donations').properties, - ) - }) try { - const txResult = await writeTxResultPromise - if (!txResult[0]) return null - donations = txResult[0] + const txResult = await session.readTransaction(async (txc) => { + const donationsTransactionResponse = await txc.run( + ` + MATCH (donations:Donations) + WITH donations LIMIT 1 + RETURN donations + `, + {}, + ) + return donationsTransactionResponse.records.map( + (record) => record.get('donations').properties, + ) + }) + return txResult[0] || null } finally { - session.close() + await session.close() } - return donations }, }, Mutation: { UpdateDonations: async (_parent, params, context, _resolveInfo) => { const { driver } = context - let donations const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async (txc) => { - const updateDonationsTransactionResponse = await txc.run( - ` - MATCH (donations:Donations) - WITH donations LIMIT 1 - SET donations += $params - SET donations.updatedAt = toString(datetime()) - RETURN donations - `, - { params }, - ) - return updateDonationsTransactionResponse.records.map( - (record) => record.get('donations').properties, - ) - }) try { - const txResult = await writeTxResultPromise - if (!txResult[0]) return null - donations = txResult[0] + const txResult = await session.writeTransaction(async (txc) => { + const updateDonationsTransactionResponse = await txc.run( + ` + MATCH (donations:Donations) + WITH donations LIMIT 1 + SET donations += $params + SET donations.updatedAt = toString(datetime()) + RETURN donations + `, + { params }, + ) + return updateDonationsTransactionResponse.records.map( + (record) => record.get('donations').properties, + ) + }) + return txResult[0] || null } finally { - session.close() + await session.close() } - return donations }, }, } diff --git a/backend/src/graphql/resolvers/emails.ts b/backend/src/graphql/resolvers/emails.ts index f5c4eb0ad..c1177a8cc 100644 --- a/backend/src/graphql/resolvers/emails.ts +++ b/backend/src/graphql/resolvers/emails.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -17,27 +16,24 @@ export default { VerifyNonce: async (_parent, args, context, _resolveInfo) => { args.email = normalizeEmail(args.email) const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const result = await txc.run( - ` - MATCH (email:EmailAddress {email: $email, nonce: $nonce}) - RETURN count(email) > 0 AS result - `, - { email: args.email, nonce: args.nonce }, - ) - return result - }) try { - const txResult = await readTxResultPromise + const txResult = await session.readTransaction(async (txc) => { + return await txc.run( + ` + MATCH (email:EmailAddress {email: $email, nonce: $nonce}) + RETURN count(email) > 0 AS result + `, + { email: args.email, nonce: args.nonce }, + ) + }) return txResult.records[0].get('result') } finally { - session.close() + await session.close() } }, }, Mutation: { AddEmailAddress: async (_parent, args, context, _resolveInfo) => { - let response args.email = normalizeEmail(args.email) try { const { neode } = context @@ -57,65 +53,64 @@ export default { } = context const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (txc) => { - const result = await txc.run( - ` - MATCH (user:User {id: $userId}) - MERGE (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce}) - SET email.createdAt = toString(datetime()) - RETURN email, user - `, - { userId, email: args.email, nonce }, - ) - return result.records.map((record) => ({ - name: record.get('user').properties.name, - locale: record.get('user').properties.locale, - ...record.get('email').properties, - })) - }) try { - const txResult = await writeTxResultPromise - response = txResult[0] + const txResult = await session.writeTransaction(async (txc) => { + const result = await txc.run( + ` + MATCH (user:User {id: $userId}) + MERGE (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce}) + SET email.createdAt = toString(datetime()) + RETURN email, user + `, + { userId, email: args.email, nonce }, + ) + return result.records.map((record) => ({ + name: record.get('user').properties.name, + locale: record.get('user').properties.locale, + ...record.get('email').properties, + })) + }) + const response = txResult[0] + if (!response) throw new UserInputError('User not found.') + return response } finally { - session.close() + await session.close() } - return response }, VerifyEmailAddress: async (_parent, args, context, _resolveInfo) => { - let response const { user: { id: userId }, } = context args.email = normalizeEmail(args.email) const { nonce, email } = args const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (txc) => { - const result = await txc.run( - ` - MATCH (user:User {id: $userId})-[:PRIMARY_EMAIL]->(previous:EmailAddress) - MATCH (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce}) - OPTIONAL MATCH (abandonedEmail:EmailAddress{email: $email}) WHERE NOT EXISTS ((abandonedEmail)<-[]-()) - DELETE abandonedEmail - MERGE (user)-[:PRIMARY_EMAIL]->(email) - SET email:EmailAddress - SET email.verifiedAt = toString(datetime()) - REMOVE email:UnverifiedEmailAddress - DETACH DELETE previous - RETURN email - `, - { userId, email, nonce }, - ) - return result.records.map((record) => record.get('email').properties) - }) + let response try { - const txResult = await writeTxResultPromise + const txResult = await session.writeTransaction(async (txc) => { + const result = await txc.run( + ` + MATCH (user:User {id: $userId})-[:PRIMARY_EMAIL]->(previous:EmailAddress) + MATCH (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce}) + OPTIONAL MATCH (abandonedEmail:EmailAddress{email: $email}) WHERE NOT EXISTS ((abandonedEmail)<-[]-()) + DELETE abandonedEmail + MERGE (user)-[:PRIMARY_EMAIL]->(email) + SET email:EmailAddress + SET email.verifiedAt = toString(datetime()) + REMOVE email:UnverifiedEmailAddress + DETACH DELETE previous + RETURN email + `, + { userId, email, nonce }, + ) + return result.records.map((record) => record.get('email').properties) + }) response = txResult[0] } catch (e) { if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') throw new UserInputError('A user account with this email already exists.') - throw new Error(e) + throw e } finally { - session.close() + await session.close() } if (!response) throw new UserInputError('Invalid nonce or no email address found.') return response diff --git a/backend/src/graphql/resolvers/groups.ts b/backend/src/graphql/resolvers/groups.ts index 351143009..bbdf6ed7f 100644 --- a/backend/src/graphql/resolvers/groups.ts +++ b/backend/src/graphql/resolvers/groups.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ @@ -13,10 +12,7 @@ import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '@constants/groups' import { removeHtmlTags } from '@middleware/helpers/cleanHtml' import type { Context } from '@src/context' -import Resolver, { - removeUndefinedNullValuesFromObject, - convertObjectToCypherMapLiteral, -} from './helpers/Resolver' +import Resolver from './helpers/Resolver' import { images } from './images/images' import { createOrUpdateLocations } from './users/location' @@ -24,35 +20,40 @@ export default { Query: { Group: async (_object, params, context: Context, _resolveInfo) => { const { isMember, id, slug, first, offset } = params - const matchParams = { id, slug } - removeUndefinedNullValuesFromObject(matchParams) const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - if (!context.user) { - throw new Error('Missing authenticated user.') - } - const transactionResponse = await txc.run( - ` - MATCH (group:Group${convertObjectToCypherMapLiteral(matchParams, true)}) - OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) - WITH group, membership - ${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''} - ${(isMember === false && "WHERE membership IS NULL AND (group.groupType IN ['public', 'closed'])") || ''} - ${(isMember === undefined && "WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''} - RETURN group {.*, myRole: membership.role} - ORDER BY group.createdAt DESC - ${first !== undefined && offset !== undefined ? `SKIP ${offset} LIMIT ${first}` : ''} - `, - { - userId: context.user.id, - }, - ) - return transactionResponse.records.map((record) => record.get('group')) - }) try { - return await readTxResultPromise - } catch (error) { - throw new Error(error) + return await session.readTransaction(async (txc) => { + if (!context.user) { + throw new Error('Missing authenticated user.') + } + const matchFilters: string[] = [] + if (id !== undefined) matchFilters.push('group.id = $id') + if (slug !== undefined) matchFilters.push('group.slug = $slug') + const matchWhere = matchFilters.length ? `WHERE ${matchFilters.join(' AND ')}` : '' + + const transactionResponse = await txc.run( + ` + MATCH (group:Group) + ${matchWhere} + OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) + WITH group, membership + ${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''} + ${(isMember === false && "WHERE membership IS NULL AND (group.groupType IN ['public', 'closed'])") || ''} + ${(isMember === undefined && "WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''} + RETURN group {.*, myRole: membership.role} + ORDER BY group.createdAt DESC + ${first !== undefined && offset !== undefined ? 'SKIP toInteger($offset) LIMIT toInteger($first)' : ''} + `, + { + userId: context.user.id, + id, + slug, + first, + offset, + }, + ) + return transactionResponse.records.map((record) => record.get('group')) + }) } finally { await session.close() } @@ -60,25 +61,22 @@ export default { GroupMembers: async (_object, params, context: Context, _resolveInfo) => { const { id: groupId, first = 25, offset = 0 } = params const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const groupMemberCypher = ` - MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId}) - RETURN user {.*}, membership {.*} - SKIP toInteger($offset) LIMIT toInteger($first) - ` - const transactionResponse = await txc.run(groupMemberCypher, { - groupId, - first, - offset, - }) - return transactionResponse.records.map((record) => { - return { user: record.get('user'), membership: record.get('membership') } - }) - }) try { - return await readTxResultPromise - } catch (error) { - throw new Error(error) + return await session.readTransaction(async (txc) => { + const groupMemberCypher = ` + MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId}) + RETURN user {.*}, membership {.*} + SKIP toInteger($offset) LIMIT toInteger($first) + ` + const transactionResponse = await txc.run(groupMemberCypher, { + groupId, + first, + offset, + }) + return transactionResponse.records.map((record) => { + return { user: record.get('user'), membership: record.get('membership') } + }) + }) } finally { await session.close() } @@ -89,31 +87,29 @@ export default { user: { id: userId }, } = context const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - let cypher - if (isMember) { - cypher = `MATCH (user:User)-[membership:MEMBER_OF]->(group:Group) - WHERE user.id = $userId - AND membership.role IN ['usual', 'admin', 'owner', 'pending'] - RETURN toString(count(group)) AS count` - } else { - cypher = `MATCH (group:Group) - OPTIONAL MATCH (user:User)-[membership:MEMBER_OF]->(group) - WHERE user.id = $userId - WITH group, membership - WHERE group.groupType IN ['public', 'closed'] - OR membership.role IN ['usual', 'admin', 'owner'] - RETURN toString(count(group)) AS count` - } - const transactionResponse = await txc.run(cypher, { userId }) - return transactionResponse.records.map((record) => record.get('count')) - }) try { - return parseInt(await readTxResultPromise) - } catch (error) { - throw new Error(error) + const result = await session.readTransaction(async (txc) => { + let cypher + if (isMember) { + cypher = `MATCH (user:User)-[membership:MEMBER_OF]->(group:Group) + WHERE user.id = $userId + AND membership.role IN ['usual', 'admin', 'owner', 'pending'] + RETURN toString(count(group)) AS count` + } else { + cypher = `MATCH (group:Group) + OPTIONAL MATCH (user:User)-[membership:MEMBER_OF]->(group) + WHERE user.id = $userId + WITH group, membership + WHERE group.groupType IN ['public', 'closed'] + OR membership.role IN ['usual', 'admin', 'owner'] + RETURN toString(count(group)) AS count` + } + const transactionResponse = await txc.run(cypher, { userId }) + return transactionResponse.records.map((record) => record.get('count'))[0] + }) + return parseInt(result, 10) || 0 } finally { - session.close() + await session.close() } }, }, @@ -138,52 +134,51 @@ export default { } params.id = params.id || uuid() const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - if (!context.user) { - throw new Error('Missing authenticated user.') - } - const categoriesCypher = - config.CATEGORIES_ACTIVE && categoryIds - ? ` - WITH group, membership - UNWIND $categoryIds AS categoryId - MATCH (category:Category {id: categoryId}) - MERGE (group)-[:CATEGORIZED]->(category) - ` - : '' - const ownerCreateGroupTransactionResponse = await transaction.run( - ` - CREATE (group:Group) - SET group += $params - SET group.createdAt = toString(datetime()) - SET group.updatedAt = toString(datetime()) - WITH group - MATCH (owner:User {id: $userId}) - MERGE (owner)-[:CREATED]->(group) - MERGE (owner)-[membership:MEMBER_OF]->(group) - SET - membership.createdAt = toString(datetime()), - membership.updatedAt = null, - membership.role = 'owner' - ${categoriesCypher} - RETURN group {.*, myRole: membership.role} - `, - { userId: context.user.id, categoryIds, params }, - ) - const [group] = ownerCreateGroupTransactionResponse.records.map((record) => - record.get('group'), - ) - return group - }) try { - const group = await writeTxResultPromise + const group = await session.writeTransaction(async (transaction) => { + if (!context.user) { + throw new Error('Missing authenticated user.') + } + const categoriesCypher = + config.CATEGORIES_ACTIVE && categoryIds + ? ` + WITH group, membership + UNWIND $categoryIds AS categoryId + MATCH (category:Category {id: categoryId}) + MERGE (group)-[:CATEGORIZED]->(category) + ` + : '' + const ownerCreateGroupTransactionResponse = await transaction.run( + ` + CREATE (group:Group) + SET group += $params + SET group.createdAt = toString(datetime()) + SET group.updatedAt = toString(datetime()) + WITH group + MATCH (owner:User {id: $userId}) + MERGE (owner)-[:CREATED]->(group) + MERGE (owner)-[membership:MEMBER_OF]->(group) + SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = 'owner' + ${categoriesCypher} + RETURN group {.*, myRole: membership.role} + `, + { userId: context.user.id, categoryIds, params }, + ) + const [group] = ownerCreateGroupTransactionResponse.records.map((record) => + record.get('group'), + ) + return group + }) // TODO: put in a middleware, see "UpdateGroup", "UpdateUser" await createOrUpdateLocations('Group', params.id, params.locationName, session, context) return group } catch (error) { if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') throw new UserInputError('Group with this slug already exists!') - throw new Error(error) + throw error } finally { await session.close() } @@ -211,61 +206,59 @@ export default { throw new UserInputError('Description too short!') } const session = context.driver.session() - if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { - const cypherDeletePreviousRelations = ` - MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category) - DELETE previousRelations - RETURN group, category - ` - await session.writeTransaction((transaction) => { - return transaction.run(cypherDeletePreviousRelations, { groupId }) - }) - } - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - if (!context.user) { - throw new Error('Missing authenticated user.') - } - let updateGroupCypher = ` - MATCH (group:Group {id: $groupId}) - SET group += $params - SET group.updatedAt = toString(datetime()) - WITH group - ` - if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { - updateGroupCypher += ` - UNWIND $categoryIds AS categoryId - MATCH (category:Category {id: categoryId}) - MERGE (group)-[:CATEGORIZED]->(category) + try { + const group = await session.writeTransaction(async (transaction) => { + if (!context.user) { + throw new Error('Missing authenticated user.') + } + if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { + await transaction.run( + ` + MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(:Category) + DELETE previousRelations + `, + { groupId }, + ) + } + let updateGroupCypher = ` + MATCH (group:Group {id: $groupId}) + SET group += $params + SET group.updatedAt = toString(datetime()) WITH group ` - } - updateGroupCypher += ` - OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) - RETURN group {.*, myRole: membership.role} - ` - const transactionResponse = await transaction.run(updateGroupCypher, { - groupId, - userId: context.user.id, - categoryIds, - params, - }) - const [group] = transactionResponse.records.map((record) => record.get('group')) - if (avatarInput) { - await images(context.config).mergeImage(group, 'AVATAR_IMAGE', avatarInput, { - transaction, + if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { + updateGroupCypher += ` + UNWIND $categoryIds AS categoryId + MATCH (category:Category {id: categoryId}) + MERGE (group)-[:CATEGORIZED]->(category) + WITH group + ` + } + updateGroupCypher += ` + OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) + RETURN group {.*, myRole: membership.role} + ` + const transactionResponse = await transaction.run(updateGroupCypher, { + groupId, + userId: context.user.id, + categoryIds, + params, }) - } - return group - }) - try { - const group = await writeTxResultPromise + const [group] = transactionResponse.records.map((record) => record.get('group')) + if (avatarInput) { + await images(context.config).mergeImage(group, 'AVATAR_IMAGE', avatarInput, { + transaction, + }) + } + return group + }) // TODO: put in a middleware, see "CreateGroup", "UpdateUser" await createOrUpdateLocations('Group', params.id, params.locationName, session, context) return group } catch (error) { if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') throw new UserInputError('Group with this slug already exists!') - throw new Error(error) + throw error } finally { await session.close() } @@ -273,29 +266,30 @@ export default { JoinGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const joinGroupCypher = ` - MATCH (user:User {id: $userId}), (group:Group {id: $groupId}) - MERGE (user)-[membership:MEMBER_OF]->(group) - ON CREATE SET - membership.createdAt = toString(datetime()), - membership.updatedAt = null, - membership.role = - CASE WHEN group.groupType = 'public' - THEN 'usual' - ELSE 'pending' - END - RETURN user {.*}, membership {.*} - ` - const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId }) - return transactionResponse.records.map((record) => { - return { user: record.get('user'), membership: record.get('membership') } - }) - }) try { - return (await writeTxResultPromise)[0] - } catch (error) { - throw new Error(error) + const result = await session.writeTransaction(async (transaction) => { + const joinGroupCypher = ` + MATCH (user:User {id: $userId}), (group:Group {id: $groupId}) + MERGE (user)-[membership:MEMBER_OF]->(group) + ON CREATE SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = + CASE WHEN group.groupType = 'public' + THEN 'usual' + ELSE 'pending' + END + RETURN user {.*}, membership {.*} + ` + const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId }) + return transactionResponse.records.map((record) => { + return { user: record.get('user'), membership: record.get('membership') } + }) + }) + if (!result[0]) { + throw new UserInputError('Could not find User or Group') + } + return result[0] } finally { await session.close() } @@ -305,8 +299,6 @@ export default { const session = context.driver.session() try { return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) - } catch (error) { - throw new Error(error) } finally { await session.close() } @@ -314,49 +306,46 @@ export default { ChangeGroupMemberRole: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId, roleInGroup } = params const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - let postRestrictionCypher = '' - if (['usual', 'admin', 'owner'].includes(roleInGroup)) { - postRestrictionCypher = ` - WITH group, member, membership - FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | - DELETE restriction)` - } else { - postRestrictionCypher = ` - WITH group, member, membership - FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | - MERGE (member)-[:CANNOT_SEE]->(post))` - } - - const joinGroupCypher = ` - MATCH (member:User {id: $userId}) - MATCH (group:Group {id: $groupId}) - MERGE (member)-[membership:MEMBER_OF]->(group) - ON CREATE SET - membership.createdAt = toString(datetime()), - membership.updatedAt = null, - membership.role = $roleInGroup - ON MATCH SET - membership.updatedAt = toString(datetime()), - membership.role = $roleInGroup - ${postRestrictionCypher} - RETURN member {.*} as user, membership {.*} - ` - - const transactionResponse = await transaction.run(joinGroupCypher, { - groupId, - userId, - roleInGroup, - }) - const [member] = transactionResponse.records.map((record) => { - return { user: record.get('user'), membership: record.get('membership') } - }) - return member - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + let postRestrictionCypher = '' + if (['usual', 'admin', 'owner'].includes(roleInGroup)) { + postRestrictionCypher = ` + WITH group, member, membership + FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | + DELETE restriction)` + } else { + postRestrictionCypher = ` + With group, member, membership + FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | + MERGE (member)-[:CANNOT_SEE]->(post))` + } + + const joinGroupCypher = ` + MATCH (member:User {id: $userId}) + MATCH (group:Group {id: $groupId}) + MERGE (member)-[membership:MEMBER_OF]->(group) + ON CREATE SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = $roleInGroup + ON MATCH SET + membership.updatedAt = toString(datetime()), + membership.role = $roleInGroup + ${postRestrictionCypher} + RETURN member {.*} as user, membership {.*} + ` + + const transactionResponse = await transaction.run(joinGroupCypher, { + groupId, + userId, + roleInGroup, + }) + const [member] = transactionResponse.records.map((record) => { + return { user: record.get('user'), membership: record.get('membership') } + }) + return member + }) } finally { await session.close() } @@ -366,8 +355,6 @@ export default { const session = context.driver.session() try { return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) - } catch (error) { - throw new Error(error) } finally { await session.close() } @@ -379,30 +366,24 @@ export default { const { groupId } = params const userId = context.user.id const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - if (!context.user) { - throw new Error('Missing authenticated user.') - } - const transactionResponse = await transaction.run( - ` - MATCH (group:Group { id: $groupId }) - MATCH (user:User { id: $userId }) - MERGE (user)-[m:MUTED]->(group) - SET m.createdAt = toString(datetime()) - RETURN group { .* } - `, - { - groupId, - userId, - }, - ) - const [group] = transactionResponse.records.map((record) => record.get('group')) - return group - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group { id: $groupId }) + MATCH (user:User { id: $userId }) + MERGE (user)-[m:MUTED]->(group) + SET m.createdAt = toString(datetime()) + RETURN group { .* } + `, + { + groupId, + userId, + }, + ) + const [group] = transactionResponse.records.map((record) => record.get('group')) + return group + }) } finally { await session.close() } @@ -414,27 +395,24 @@ export default { const { groupId } = params const userId = context.user.id const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const transactionResponse = await transaction.run( - ` - MATCH (group:Group { id: $groupId }) - MATCH (user:User { id: $userId }) - OPTIONAL MATCH (user)-[m:MUTED]->(group) - DELETE m - RETURN group { .* } - `, - { - groupId, - userId, - }, - ) - const [group] = transactionResponse.records.map((record) => record.get('group')) - return group - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group { id: $groupId }) + MATCH (user:User { id: $userId }) + OPTIONAL MATCH (user)-[m:MUTED]->(group) + DELETE m + RETURN group { .* } + `, + { + groupId, + userId, + }, + ) + const [group] = transactionResponse.records.map((record) => record.get('group')) + return group + }) } finally { await session.close() } @@ -540,9 +518,12 @@ const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) groupId, userId, }) - const [user] = await transactionResponse.records.map((record) => { + const [result] = transactionResponse.records.map((record) => { return { user: record.get('user'), membership: record.get('membership') } }) - return user + if (!result) { + throw new UserInputError('User is not a member of this group') + } + return result }) } diff --git a/backend/src/graphql/resolvers/helpers/Resolver.ts b/backend/src/graphql/resolvers/helpers/Resolver.ts index 71d7602a4..22eb8dc51 100644 --- a/backend/src/graphql/resolvers/helpers/Resolver.ts +++ b/backend/src/graphql/resolvers/helpers/Resolver.ts @@ -33,20 +33,19 @@ export default function Resolver(type, options: any = {}) { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const cypher = ` - MATCH(:${type} {${idAttribute}: $id})${connection} - RETURN related {.*} as related - ` - const result = await txc.run(cypher, { id, cypherParams }) - return result.records.map((r) => r.get('related')) - }) try { - let response = await readTxResultPromise + let response = await session.readTransaction(async (txc) => { + const cypher = ` + MATCH(:${type} {${idAttribute}: $id})${connection} + RETURN related {.*} as related + ` + const result = await txc.run(cypher, { id, cypherParams }) + return result.records.map((r) => r.get('related')) + }) if (returnType === 'object') response = response[0] || null return response } finally { - session.close() + await session.close() } } } @@ -59,17 +58,16 @@ export default function Resolver(type, options: any = {}) { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const nodeCondition = condition.replace('this', 'this {id: $id}') - const cypher = `${nodeCondition} as ${key}` - const result = await txc.run(cypher, { id, cypherParams }) - const [response] = result.records.map((r) => r.get(key)) - return response - }) try { - return await readTxResultPromise + return await session.readTransaction(async (txc) => { + const nodeCondition = condition.replace('this', 'this {id: $id}') + const cypher = `${nodeCondition} as ${key}` + const result = await txc.run(cypher, { id, cypherParams }) + const [response] = result.records.map((r) => r.get(key)) + return response + }) } finally { - session.close() + await session.close() } } } @@ -82,20 +80,19 @@ export default function Resolver(type, options: any = {}) { resolvers[key] = async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const session = driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const id = parent[idAttribute] - const cypher = ` - MATCH(u:${type} {${idAttribute}: $id})${connection} - RETURN COUNT(DISTINCT(related)) as count - ` - const result = await txc.run(cypher, { id, cypherParams }) - const [response] = result.records.map((r) => r.get('count').toNumber()) - return response - }) try { - return await readTxResultPromise + return await session.readTransaction(async (txc) => { + const id = parent[idAttribute] + const cypher = ` + MATCH(u:${type} {${idAttribute}: $id})${connection} + RETURN COUNT(DISTINCT(related)) as count + ` + const result = await txc.run(cypher, { id, cypherParams }) + const [response] = result.records.map((r) => r.get('count').toNumber()) + return response + }) } finally { - session.close() + await session.close() } } } diff --git a/backend/src/graphql/resolvers/helpers/existingEmailAddress.ts b/backend/src/graphql/resolvers/helpers/existingEmailAddress.ts index 03d902881..cf04a5495 100644 --- a/backend/src/graphql/resolvers/helpers/existingEmailAddress.ts +++ b/backend/src/graphql/resolvers/helpers/existingEmailAddress.ts @@ -8,7 +8,7 @@ export default async function alreadyExistingMail({ args, context }) { args.email = normalizeEmail(args.email) const session = context.driver.session() try { - const existingEmailAddressTxPromise = session.writeTransaction(async (transaction) => { + const result = await session.readTransaction(async (transaction) => { const existingEmailAddressTransactionResponse = await transaction.run( ` MATCH (email:EmailAddress {email: $email}) @@ -24,13 +24,13 @@ export default async function alreadyExistingMail({ args, context }) { } }) }) - const [emailBelongsToUser] = await existingEmailAddressTxPromise + const [emailBelongsToUser] = result /* const { alreadyExistingEmail, user } = if (user) throw new UserInputError('A user account with this email already exists.') */ return emailBelongsToUser || {} } finally { - session.close() + await session.close() } } diff --git a/backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts b/backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts index 2a264ced4..d429e6cf9 100644 --- a/backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts +++ b/backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts @@ -6,29 +6,29 @@ import { mergeWith, isArray } from 'lodash' const getInvisiblePosts = async (context) => { const session = context.driver.session() - const readTxResultPromise = await session.readTransaction(async (transaction) => { - let cypher = '' - const { user } = context - if (user?.id) { - cypher = ` - MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) - RETURN collect(post.id) AS invisiblePostIds` - } else { - cypher = ` - MATCH (post:Post)-[:IN]->(group:Group) - WHERE NOT group.groupType = 'public' - RETURN collect(post.id) AS invisiblePostIds` - } - const invisiblePostIdsResponse = await transaction.run(cypher, { - userId: user ? user.id : null, - }) - return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds')) - }) try { - const [invisiblePostIds] = readTxResultPromise + const readTxResult = await session.readTransaction(async (transaction) => { + let cypher = '' + const { user } = context + if (user?.id) { + cypher = ` + MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) + RETURN collect(post.id) AS invisiblePostIds` + } else { + cypher = ` + MATCH (post:Post)-[:IN]->(group:Group) + WHERE NOT group.groupType = 'public' + RETURN collect(post.id) AS invisiblePostIds` + } + const invisiblePostIdsResponse = await transaction.run(cypher, { + userId: user ? user.id : null, + }) + return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds')) + }) + const [invisiblePostIds] = readTxResult return invisiblePostIds } finally { - session.close() + await session.close() } } diff --git a/backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts b/backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts index 9d40b097e..e092c3fd2 100644 --- a/backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts +++ b/backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts @@ -9,19 +9,19 @@ const getMyGroupIds = async (context) => { if (!user?.id) return [] const session = context.driver.session() - const readTxResultPromise = await session.readTransaction(async (transaction) => { - const cypher = ` - MATCH (group:Group)<-[membership:MEMBER_OF]-(:User { id: $userId }) - WHERE membership.role IN ['usual', 'admin', 'owner'] - RETURN collect(group.id) AS myGroupIds` - const getMyGroupIdsResponse = await transaction.run(cypher, { userId: user.id }) - return getMyGroupIdsResponse.records.map((record) => record.get('myGroupIds')) - }) try { - const [myGroupIds] = readTxResultPromise + const readTxResult = await session.readTransaction(async (transaction) => { + const cypher = ` + MATCH (group:Group)<-[membership:MEMBER_OF]-(:User { id: $userId }) + WHERE membership.role IN ['usual', 'admin', 'owner'] + RETURN collect(group.id) AS myGroupIds` + const getMyGroupIdsResponse = await transaction.run(cypher, { userId: user.id }) + return getMyGroupIdsResponse.records.map((record) => record.get('myGroupIds')) + }) + const [myGroupIds] = readTxResult return myGroupIds } finally { - session.close() + await session.close() } } diff --git a/backend/src/graphql/resolvers/messages.ts b/backend/src/graphql/resolvers/messages.ts index 0898f7016..4af6b8862 100644 --- a/backend/src/graphql/resolvers/messages.ts +++ b/backend/src/graphql/resolvers/messages.ts @@ -65,7 +65,7 @@ export default { await setMessagesAsDistributed(undistributedMessagesIds, session) } } finally { - session.close() + await session.close() } // send subscription to author to updated the messages } @@ -82,43 +82,41 @@ export default { const session = context.driver.session() try { - const writeTxResultPromise = session.writeTransaction(async (transaction) => { + return await session.writeTransaction(async (transaction) => { const createMessageCypher = ` - MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) - OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image) - OPTIONAL MATCH (m:Message)-[:INSIDE]->(room) - OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User) - WHERE NOT recipientUser.id = $currentUserId - WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser - CREATE (currentUser)-[:CREATED]->(message:Message { - createdAt: toString(datetime()), - id: apoc.create.uuid(), - indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END, - content: LEFT($content,2000), - saved: true, - distributed: false, - seen: false - })-[:INSIDE]->(room) - SET room.lastMessageAt = toString(datetime()) - RETURN message { - .*, - indexId: toString(message.indexId), - recipientId: recipientUser.id, - senderId: currentUser.id, - username: currentUser.name, - avatar: image.url, - date: message.createdAt - } - ` + MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) + OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image) + OPTIONAL MATCH (m:Message)-[:INSIDE]->(room) + OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User) + WHERE NOT recipientUser.id = $currentUserId + WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser + CREATE (currentUser)-[:CREATED]->(message:Message { + createdAt: toString(datetime()), + id: apoc.create.uuid(), + indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END, + content: LEFT($content,2000), + saved: true, + distributed: false, + seen: false + })-[:INSIDE]->(room) + SET room.lastMessageAt = toString(datetime()) + RETURN message { + .*, + indexId: toString(message.indexId), + recipientId: recipientUser.id, + senderId: currentUser.id, + username: currentUser.name, + avatar: image.url, + date: message.createdAt + } + ` const createMessageTxResponse = await transaction.run(createMessageCypher, { currentUserId, roomId, content, }) - const [message] = await createMessageTxResponse.records.map((record) => - record.get('message'), - ) + const [message] = createMessageTxResponse.records.map((record) => record.get('message')) // this is the case if the room doesn't exist - requires refactoring for implicit rooms if (!message) { @@ -142,38 +140,32 @@ export default { return { ...message, files: atns } }) - - return await writeTxResultPromise - } catch (error) { - throw new Error(error) } finally { - session.close() + await session.close() } }, MarkMessagesAsSeen: async (_parent, params, context, _resolveInfo) => { const { messageIds } = params const currentUserId = context.user.id const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const setSeenCypher = ` - MATCH (m:Message)<-[:CREATED]-(user:User) - WHERE m.id IN $messageIds AND NOT user.id = $currentUserId - SET m.seen = true - RETURN m { .* } - ` - const setSeenTxResponse = await transaction.run(setSeenCypher, { - messageIds, - currentUserId, - }) - const messages = await setSeenTxResponse.records.map((record) => record.get('m')) - return messages - }) try { - await writeTxResultPromise + await session.writeTransaction(async (transaction) => { + const setSeenCypher = ` + MATCH (m:Message)<-[:CREATED]-(user:User) + WHERE m.id IN $messageIds AND NOT user.id = $currentUserId + SET m.seen = true + RETURN m { .* } + ` + const setSeenTxResponse = await transaction.run(setSeenCypher, { + messageIds, + currentUserId, + }) + return setSeenTxResponse.records.map((record) => record.get('m')) + }) // send subscription to author to updated the messages return true } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/moderation.ts b/backend/src/graphql/resolvers/moderation.ts index bcdb3992a..c98cc1df3 100644 --- a/backend/src/graphql/resolvers/moderation.ts +++ b/backend/src/graphql/resolvers/moderation.ts @@ -34,7 +34,7 @@ export default { const [reviewed] = await reviewWriteTxResultPromise return reviewed || null } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/notifications.ts b/backend/src/graphql/resolvers/notifications.ts index 0c35c249e..abf594553 100644 --- a/backend/src/graphql/resolvers/notifications.ts +++ b/backend/src/graphql/resolvers/notifications.ts @@ -80,7 +80,7 @@ export default { const notifications = await readTxResultPromise return notifications } finally { - session.close() + await session.close() } }, }, @@ -111,7 +111,7 @@ export default { const [notifications] = await writeTxResultPromise return notifications } finally { - session.close() + await session.close() } }, markAllAsRead: async (parent, args, context, _resolveInfo) => { @@ -140,7 +140,7 @@ export default { const notifications = await writeTxResultPromise return notifications } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/passwordReset.ts b/backend/src/graphql/resolvers/passwordReset.ts index fb602f276..53097268b 100644 --- a/backend/src/graphql/resolvers/passwordReset.ts +++ b/backend/src/graphql/resolvers/passwordReset.ts @@ -54,7 +54,7 @@ export default { const [reset] = await passwordResetTxPromise return !!reset?.properties.usedAt } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/posts.ts b/backend/src/graphql/resolvers/posts.ts index b63bdee77..a194619c6 100644 --- a/backend/src/graphql/resolvers/posts.ts +++ b/backend/src/graphql/resolvers/posts.ts @@ -88,7 +88,7 @@ export default { const [emotionsCount] = await readTxResultPromise return emotionsCount } finally { - session.close() + await session.close() } }, PostsEmotionsByCurrentUser: async (_object, params, context: Context, _resolveInfo) => { @@ -364,7 +364,7 @@ export default { const [emoted] = await writeTxResultPromise return emoted } finally { - session.close() + await session.close() } }, pinPost: async (_parent, params, context: Context, _resolveInfo) => { @@ -464,7 +464,7 @@ export default { try { ;[unpinnedPost] = await writeTxResultPromise } finally { - session.close() + await session.close() } return unpinnedPost }, @@ -552,7 +552,7 @@ export default { post.viewedTeaserCount = post.viewedTeaserCount.low return post } finally { - session.close() + await session.close() } }, toggleObservePost: async (_parent, params, context, _resolveInfo) => { @@ -581,7 +581,7 @@ export default { post.viewedTeaserCount = post.viewedTeaserCount.low return post } finally { - session.close() + await session.close() } }, pushPost: async (_parent, params, context: Context, _resolveInfo) => { @@ -705,7 +705,7 @@ export default { const relatedContributions = await writeTxResultPromise return relatedContributions } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/reports.ts b/backend/src/graphql/resolvers/reports.ts index fb619e44b..f9b3a3b63 100644 --- a/backend/src/graphql/resolvers/reports.ts +++ b/backend/src/graphql/resolvers/reports.ts @@ -37,7 +37,7 @@ export default { const [filedReport] = await fileReportWriteTxResultPromise return filedReport || null } finally { - session.close() + await session.close() } }, }, @@ -108,7 +108,7 @@ export default { const reports = await reportsReadTxPromise return reports || [] } finally { - session.close() + await session.close() } }, }, @@ -143,7 +143,7 @@ export default { return relationshipWithNestedAttributes }) } finally { - session.close() + await session.close() } return filed }, */ @@ -177,7 +177,7 @@ export default { return relationshipWithNestedAttributes }) } finally { - session.close() + await session.close() } return reviewed }, diff --git a/backend/src/graphql/resolvers/rooms.ts b/backend/src/graphql/resolvers/rooms.ts index e3422a5ce..d986a9b32 100644 --- a/backend/src/graphql/resolvers/rooms.ts +++ b/backend/src/graphql/resolvers/rooms.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ @@ -53,7 +52,7 @@ export default { const count = await getUnreadRoomsCount(currentUserId, session) return count } finally { - session.close() + await session.close() } }, }, @@ -67,43 +66,40 @@ export default { throw new Error('Cannot create a room with self') } const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const createRoomCypher = ` - MATCH (currentUser:User { id: $currentUserId }) - MATCH (user:User { id: $userId }) - MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) - ON CREATE SET - room.createdAt = toString(datetime()), - room.id = apoc.create.uuid() - WITH room, user, currentUser - OPTIONAL MATCH (room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) - WHERE NOT sender.id = $currentUserId AND NOT message.seen - WITH room, user, currentUser, message, - user.name AS roomName - RETURN room { - .*, - users: [properties(currentUser), properties(user)], - roomName: roomName, - unreadCount: toString(COUNT(DISTINCT message)) - } - ` - const createRommTxResponse = await transaction.run(createRoomCypher, { - userId, - currentUserId, - }) - const [room] = await createRommTxResponse.records.map((record) => record.get('room')) - return room - }) try { - const room = await writeTxResultPromise + const room = await session.writeTransaction(async (transaction) => { + const createRoomCypher = ` + MATCH (currentUser:User { id: $currentUserId }) + MATCH (user:User { id: $userId }) + MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) + ON CREATE SET + room.createdAt = toString(datetime()), + room.id = apoc.create.uuid() + WITH room, user, currentUser + OPTIONAL MATCH (room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + WHERE NOT sender.id = $currentUserId AND NOT message.seen + WITH room, user, currentUser, message, + user.name AS roomName + RETURN room { + .*, + users: [properties(currentUser), properties(user)], + roomName: roomName, + unreadCount: toString(COUNT(DISTINCT message)) + } + ` + const createRoomTxResponse = await transaction.run(createRoomCypher, { + userId, + currentUserId, + }) + const [room] = createRoomTxResponse.records.map((record) => record.get('room')) + return room + }) if (room) { room.roomId = room.id } return room - } catch (error) { - throw new Error(error) } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/searches.ts b/backend/src/graphql/resolvers/searches.ts index 34fc11709..54ac9152d 100644 --- a/backend/src/graphql/resolvers/searches.ts +++ b/backend/src/graphql/resolvers/searches.ts @@ -134,7 +134,7 @@ const getSearchResults = async (context, setup, params, resultCallback = searchR const results = await searchResultPromise(session, setup, params) return resultCallback(results) } finally { - session.close() + await session.close() } } diff --git a/backend/src/graphql/resolvers/shout.ts b/backend/src/graphql/resolvers/shout.ts index f0b5885eb..abeed5d30 100644 --- a/backend/src/graphql/resolvers/shout.ts +++ b/backend/src/graphql/resolvers/shout.ts @@ -28,7 +28,7 @@ export default { const [isShouted] = await shoutWriteTxResultPromise return isShouted } finally { - session.close() + await session.close() } }, @@ -55,7 +55,7 @@ export default { const [isShouted] = await unshoutWriteTxResultPromise return isShouted } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/userData.ts b/backend/src/graphql/resolvers/userData.ts index 15c65b59b..feccb55b3 100644 --- a/backend/src/graphql/resolvers/userData.ts +++ b/backend/src/graphql/resolvers/userData.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -39,21 +38,16 @@ export default { }) } AS result` const session = context.driver.session() - const resultPromise = session.readTransaction(async (transaction) => { - const transactionResponse = transaction.run(cypher, { - id, - }) - return transactionResponse - }) - try { - const result = await resultPromise + const result = await session.readTransaction(async (transaction) => { + return await transaction.run(cypher, { id }) + }) const userData = result.records[0].get('result') userData.posts.sort(byCreationDate) userData.posts.forEach((post) => post.comments.sort(byCreationDate)) return userData } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/graphql/resolvers/users.spec.ts b/backend/src/graphql/resolvers/users.spec.ts index ca27efd7c..a7413c19b 100644 --- a/backend/src/graphql/resolvers/users.spec.ts +++ b/backend/src/graphql/resolvers/users.spec.ts @@ -1066,7 +1066,7 @@ describe('setTrophyBadgeSelected', () => { expect.objectContaining({ errors: [ expect.objectContaining({ - message: 'Error: You cannot set badges not rewarded to you.', + message: 'You cannot set badges not rewarded to you.', }), ], }), @@ -1083,7 +1083,7 @@ describe('setTrophyBadgeSelected', () => { expect.objectContaining({ errors: [ expect.objectContaining({ - message: 'Error: You cannot set badges not rewarded to you.', + message: 'You cannot set badges not rewarded to you.', }), ], }), diff --git a/backend/src/graphql/resolvers/users.ts b/backend/src/graphql/resolvers/users.ts index 84fb21678..a5ad85adb 100644 --- a/backend/src/graphql/resolvers/users.ts +++ b/backend/src/graphql/resolvers/users.ts @@ -49,21 +49,19 @@ export default { User: async (object, args, context, resolveInfo) => { if (args.email) { args.email = normalizeEmail(args.email) - let session + const session = context.driver.session() try { - session = context.driver.session() const readTxResult = await session.readTransaction((txc) => { - const result = txc.run( + return txc.run( ` - MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $args.email}) - RETURN user {.*, email: e.email}`, + MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $args.email}) + RETURN user {.*, email: e.email}`, { args }, ) - return result }) return readTxResult.records.map((r) => r.get('user')) } finally { - session.close() + await session.close() } } return neo4jgraphql(object, args, context, resolveInfo) @@ -105,28 +103,27 @@ export default { if (currentUser.id === args.id) return null const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const unBlockUserTransactionResponse = await transaction.run( - ` - MATCH (blockedUser:User {id: $args.id}) - MATCH (currentUser:User {id: $currentUser.id}) - OPTIONAL MATCH (currentUser)-[r:FOLLOWS]->(blockedUser) - DELETE r - CREATE (currentUser)-[:BLOCKED]->(blockedUser) - RETURN blockedUser {.*} - `, - { currentUser, args }, - ) - return unBlockUserTransactionResponse.records.map((record) => record.get('blockedUser'))[0] - }) try { - const blockedUser = await writeTxResultPromise + const blockedUser = await session.writeTransaction(async (transaction) => { + const blockUserResponse = await transaction.run( + ` + MATCH (blockedUser:User {id: $args.id}) + MATCH (currentUser:User {id: $currentUser.id}) + OPTIONAL MATCH (currentUser)-[r:FOLLOWS]->(blockedUser) + DELETE r + MERGE (currentUser)-[:BLOCKED]->(blockedUser) + RETURN blockedUser {.*} + `, + { currentUser, args }, + ) + return blockUserResponse.records.map((record) => record.get('blockedUser'))[0] + }) if (!blockedUser) { throw new UserInputError('Could not find User') } return blockedUser } finally { - session.close() + await session.close() } }, unblockUser: async (_object, args, context, _resolveInfo) => { @@ -134,25 +131,22 @@ export default { if (currentUser.id === args.id) return null const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const unBlockUserTransactionResponse = await transaction.run( - ` - MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(blockedUser:User {id: $args.id}) - DELETE r - RETURN blockedUser {.*} - `, - { currentUser, args }, - ) - return unBlockUserTransactionResponse.records.map((record) => record.get('blockedUser'))[0] - }) try { - const unblockedUser = await writeTxResultPromise + const unblockedUser = await session.writeTransaction(async (transaction) => { + const unblockUserResponse = await transaction.run( + ` + MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(blockedUser:User {id: $args.id}) + DELETE r + RETURN blockedUser {.*} + `, + { currentUser, args }, + ) + return unblockUserResponse.records.map((record) => record.get('blockedUser'))[0] + }) if (!unblockedUser) { - throw new Error('Could not find blocked User') + throw new UserInputError('Could not find blocked User') } return unblockedUser - } catch { - throw new UserInputError('Could not find blocked User') } finally { await session.close() } @@ -183,103 +177,100 @@ export default { } const session = context.driver.session() - - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const updateUserTransactionResponse = await transaction.run( - ` - MATCH (user:User {id: $params.id}) - SET user += $params - SET user.updatedAt = toString(datetime()) - RETURN user {.*} - `, - { params }, - ) - const [user] = updateUserTransactionResponse.records.map((record) => record.get('user')) - if (avatarInput) { - await images(context.config).mergeImage(user, 'AVATAR_IMAGE', avatarInput, { - transaction, - }) - } - return user - }) try { - const user = await writeTxResultPromise + const user = await session.writeTransaction(async (transaction) => { + const updateUserTransactionResponse = await transaction.run( + ` + MATCH (user:User {id: $params.id}) + SET user += $params + SET user.updatedAt = toString(datetime()) + RETURN user {.*} + `, + { params }, + ) + const [user] = updateUserTransactionResponse.records.map((record) => record.get('user')) + if (avatarInput) { + await images(context.config).mergeImage(user, 'AVATAR_IMAGE', avatarInput, { + transaction, + }) + } + return user + }) // TODO: put in a middleware, see "CreateGroup", "UpdateGroup" await createOrUpdateLocations('User', params.id, params.locationName, session, context) return user - } catch (error) { - throw new UserInputError(error.message) } finally { await session.close() } }, DeleteUser: async (_object, params, context: Context, _resolveInfo) => { const { resource, id: userId } = params + const allowedLabels = ['Post', 'Comment'] const session = context.driver.session() - - const deleteUserTxResultPromise = session.writeTransaction(async (transaction) => { - if (resource?.length) { - await Promise.all( - resource.map(async (node) => { - const txResult = await transaction.run( - ` - MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId}) - OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment) - SET resource.deleted = true - SET resource.content = 'UNAVAILABLE' - SET resource.contentExcerpt = 'UNAVAILABLE' - SET resource.language = 'UNAVAILABLE' - SET resource.createdAt = 'UNAVAILABLE' - SET resource.updatedAt = 'UNAVAILABLE' - SET comment.deleted = true - RETURN resource {.*} - `, - { - userId, - }, - ) - return Promise.all( - txResult.records - .map((record) => record.get('resource')) - .map((resource) => - images(context.config).deleteImage(resource, 'HERO_IMAGE', { transaction }), - ), - ) - }), - ) - } - - const deleteUserTransactionResponse = await transaction.run( - ` - MATCH (user:User {id: $userId}) - SET user.deleted = true - SET user.name = 'UNAVAILABLE' - SET user.about = 'UNAVAILABLE' - SET user.lastActiveAt = 'UNAVAILABLE' - SET user.createdAt = 'UNAVAILABLE' - SET user.updatedAt = 'UNAVAILABLE' - SET user.termsAndConditionsAgreedVersion = 'UNAVAILABLE' - SET user.encryptedPassword = null - WITH user - OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress) - DETACH DELETE email - 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 }, - ) - const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user')) - await images(context.config).deleteImage(user, 'AVATAR_IMAGE', { transaction }) - return user - }) try { - const user = await deleteUserTxResultPromise - return user + return await session.writeTransaction(async (transaction) => { + if (resource?.length) { + await Promise.all( + resource.map(async (node) => { + if (!allowedLabels.includes(node)) { + throw new UserInputError(`Invalid resource type: ${node}`) + } + const txResult = await transaction.run( + ` + MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId}) + OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment) + SET resource.deleted = true + SET resource.content = 'UNAVAILABLE' + SET resource.contentExcerpt = 'UNAVAILABLE' + SET resource.language = 'UNAVAILABLE' + SET resource.createdAt = 'UNAVAILABLE' + SET resource.updatedAt = 'UNAVAILABLE' + SET comment.deleted = true + RETURN resource {.*} + `, + { + userId, + }, + ) + return Promise.all( + txResult.records + .map((record) => record.get('resource')) + .map((resource) => + images(context.config).deleteImage(resource, 'HERO_IMAGE', { transaction }), + ), + ) + }), + ) + } + + const deleteUserTransactionResponse = await transaction.run( + ` + MATCH (user:User {id: $userId}) + SET user.deleted = true + SET user.name = 'UNAVAILABLE' + SET user.about = 'UNAVAILABLE' + SET user.lastActiveAt = 'UNAVAILABLE' + SET user.createdAt = 'UNAVAILABLE' + SET user.updatedAt = 'UNAVAILABLE' + SET user.termsAndConditionsAgreedVersion = 'UNAVAILABLE' + SET user.encryptedPassword = null + WITH user + OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress) + DETACH DELETE email + 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 }, + ) + const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user')) + await images(context.config).deleteImage(user, 'AVATAR_IMAGE', { transaction }) + return user + }) } finally { await session.close() } @@ -289,24 +280,26 @@ export default { if (context.user.id === id) throw new Error('you-cannot-change-your-own-role') const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const switchUserRoleResponse = await transaction.run( - ` - MATCH (user:User {id: $id}) - OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(e:EmailAddress) - SET user.role = $role - SET user.updatedAt = toString(datetime()) - RETURN user {.*, email: e.email} - `, - { id, role }, - ) - return switchUserRoleResponse.records.map((record) => record.get('user'))[0] - }) try { - const user = await writeTxResultPromise + const user = await session.writeTransaction(async (transaction) => { + const switchUserRoleResponse = await transaction.run( + ` + MATCH (user:User {id: $id}) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(e:EmailAddress) + SET user.role = $role + SET user.updatedAt = toString(datetime()) + RETURN user {.*, email: e.email} + `, + { id, role }, + ) + return switchUserRoleResponse.records.map((record) => record.get('user'))[0] + }) + if (!user) { + throw new UserInputError('Could not find User') + } return user } finally { - session.close() + await session.close() } }, saveCategorySettings: async (_object, args, context, _resolveInfo) => { @@ -316,40 +309,30 @@ export default { } = context const session = context.driver.session() - await session.writeTransaction((transaction) => { - return transaction.run( - ` - MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(category:Category) - DELETE previousCategories - RETURN user, category - `, - { id }, - ) - }) - - // frontend gives [] when all categories are selected (default) - if (activeCategories.length === 0) return true - - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const saveCategorySettingsResponse = await transaction.run( - ` - MATCH (category:Category) WHERE NOT category.id IN $activeCategories - MATCH (user:User { id: $id }) - MERGE (user)-[r:NOT_INTERESTED_IN]->(category) - RETURN user, r, category - `, - { id, activeCategories }, - ) - const [user] = await saveCategorySettingsResponse.records.map((record) => - record.get('user'), - ) - return user - }) try { - await writeTxResultPromise + await session.writeTransaction(async (transaction) => { + await transaction.run( + ` + MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(:Category) + DELETE previousCategories + `, + { id }, + ) + // frontend gives [] when all categories are selected (default) + if (activeCategories.length > 0) { + await transaction.run( + ` + MATCH (category:Category) WHERE NOT category.id IN $activeCategories + MATCH (user:User { id: $id }) + MERGE (user)-[:NOT_INTERESTED_IN]->(category) + `, + { id, activeCategories }, + ) + } + }) return true } finally { - session.close() + await session.close() } }, updateOnlineStatus: async (_object, args, context: Context, _resolveInfo) => { @@ -391,41 +374,37 @@ export default { } const session = context.driver.session() - - const query = session.writeTransaction(async (transaction) => { - const queryBadge = ` - MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId}) - OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge) - OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge) - DELETE badgeRelation, slotRelation - MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge) - RETURN user {.*} - ` - const queryEmpty = ` - MATCH (user:User {id: $userId}) - OPTIONAL MATCH (user)-[slotRelation:SELECTED {slot: $slot}]->(:Badge) - DELETE slotRelation - RETURN user {.*} - ` - const isDefault = !badgeId || badgeId === defaultTrophyBadge.id - - const result = await transaction.run(isDefault ? queryEmpty : queryBadge, { - userId, - badgeId, - slot, - }) - return result.records.map((record) => record.get('user'))[0] - }) try { - const user = await query + const user = await session.writeTransaction(async (transaction) => { + const queryBadge = ` + MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId}) + OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge) + OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge) + DELETE badgeRelation, slotRelation + MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge) + RETURN user {.*} + ` + const queryEmpty = ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[slotRelation:SELECTED {slot: $slot}]->(:Badge) + DELETE slotRelation + RETURN user {.*} + ` + const isDefault = !badgeId || badgeId === defaultTrophyBadge.id + + const result = await transaction.run(isDefault ? queryEmpty : queryBadge, { + userId, + badgeId, + slot, + }) + return result.records.map((record) => record.get('user'))[0] + }) if (!user) { - throw new Error('You cannot set badges not rewarded to you.') + throw new UserInputError('You cannot set badges not rewarded to you.') } return user - } catch (error) { - throw new Error(error) } finally { - session.close() + await session.close() } }, resetTrophyBadgesSelected: async (_object, _args, context, _resolveInfo) => { @@ -434,25 +413,21 @@ export default { } = context const session = context.driver.session() - - const query = session.writeTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $userId}) - OPTIONAL MATCH (user)-[relation:SELECTED]->(:Badge) - DELETE relation - RETURN user {.*} - `, - { userId }, - ) - return result.records.map((record) => record.get('user'))[0] - }) try { - return await query - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[relation:SELECTED]->(:Badge) + DELETE relation + RETURN user {.*} + `, + { userId }, + ) + return result.records.map((record) => record.get('user'))[0] + }) } finally { - session.close() + await session.close() } }, }, @@ -539,97 +514,80 @@ export default { }, badgeTrophiesSelected: async (parent, _params, context, _resolveInfo) => { const session = context.driver.session() - - const query = session.readTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $parent.id})-[relation:SELECTED]->(badge:Badge) - WITH relation, badge - ORDER BY relation.slot ASC - RETURN relation.slot as slot, badge {.*} - `, - { parent }, - ) - return result.records - }) try { - const badgesSelected = await query + const badgesSelected = await session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})-[relation:SELECTED]->(badge:Badge) + WITH relation, badge + ORDER BY relation.slot ASC + RETURN relation.slot as slot, badge {.*} + `, + { parent }, + ) + return result.records + }) const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(defaultTrophyBadge) - badgesSelected.map((record) => { + badgesSelected.forEach((record) => { result[record.get('slot')] = record.get('badge') - return true }) return result - } catch (error) { - throw new Error(error) } finally { - session.close() + await session.close() } }, badgeTrophiesUnused: async (parent, _params, context, _resolveInfo) => { const session = context.driver.session() - - const query = session.readTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) - WHERE NOT (user)-[:SELECTED]-(badge) - RETURN badge {.*} - `, - { parent }, - ) - return result.records.map((record) => record.get('badge')) - }) try { - return await query - } catch (error) { - throw new Error(error) + return await session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN badge {.*} + `, + { parent }, + ) + return result.records.map((record) => record.get('badge')) + }) } finally { - session.close() + await session.close() } }, badgeTrophiesUnusedCount: async (parent, _params, context, _resolveInfo) => { const session = context.driver.session() - - const query = session.readTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) - WHERE NOT (user)-[:SELECTED]-(badge) - RETURN toString(COUNT(badge)) as count - `, - { parent }, - ) - return result.records.map((record) => record.get('count'))[0] - }) try { - return await query - } catch (error) { - throw new Error(error) + return await session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN toString(COUNT(badge)) as count + `, + { parent }, + ) + return result.records.map((record) => record.get('count'))[0] + }) } finally { - session.close() + await session.close() } }, badgeVerification: async (parent, _params, context, _resolveInfo) => { const session = context.driver.session() - - const query = session.writeTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $parent.id})<-[:VERIFIES]-(verification:Badge) - RETURN verification {.*} - `, - { parent }, - ) - return result.records.map((record) => record.get('verification'))[0] - }) try { - const result = await query + const result = await session.readTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:VERIFIES]-(verification:Badge) + RETURN verification {.*} + `, + { parent }, + ) + return response.records.map((record) => record.get('verification'))[0] + }) return result ?? defaultVerificationBadge - } catch (error) { - throw new Error(error) } finally { - session.close() + await session.close() } }, ...Resolver('User', { diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.ts index 2f53ee1a5..b1bc590f4 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.ts @@ -25,7 +25,7 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => { ) }) } finally { - session.close() + await session.close() } } diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index 559c72b06..7eb359927 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -159,7 +159,7 @@ const postAuthorOfComment = async (commentId, { context }) => { }) return postAuthorId.records.map((record) => record.get('authorId')) } finally { - session.close() + await session.close() } } @@ -186,21 +186,18 @@ const notifyFollowingUsers = async (postId, groupId, context) => { } ` const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const notificationTransactionResponse = await transaction.run(cypher, { - postId, - reason, - groupId: groupId || null, - userId: context.user.id, - }) - return notificationTransactionResponse.records.map((record) => record.get('notification')) - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(cypher, { + postId, + reason, + groupId: groupId || null, + userId: context.user.id, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) } finally { - session.close() + await session.close() } } @@ -232,21 +229,18 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { } ` const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const notificationTransactionResponse = await transaction.run(cypher, { - postId, - reason, - groupId, - userId: context.user.id, - }) - return notificationTransactionResponse.records.map((record) => record.get('notification')) - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(cypher, { + postId, + reason, + groupId, + userId: context.user.id, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) } finally { - session.close() + await session.close() } } @@ -267,20 +261,17 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { RETURN notification {.*, from: finalGroup, to: properties(owner), email: email, relatedUser: properties(user) } ` const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const notificationTransactionResponse = await transaction.run(cypher, { - groupId, - reason, - userId, - }) - return notificationTransactionResponse.records.map((record) => record.get('notification')) - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(cypher, { + groupId, + reason, + userId, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) } finally { - session.close() + await session.close() } } @@ -304,21 +295,18 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { RETURN notification {.*, from: finalGroup, to: properties(user), email: email, relatedUser: properties(owner) } ` const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const notificationTransactionResponse = await transaction.run(cypher, { - groupId, - reason, - userId, - ownerId: owner.id, - }) - return notificationTransactionResponse.records.map((record) => record.get('notification')) - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(cypher, { + groupId, + reason, + userId, + ownerId: owner.id, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) } finally { - session.close() + await session.close() } } @@ -376,62 +364,58 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { RETURN notification {.*, from: finalResource, to: properties(user), email: email, relatedUser: properties(user) } ` const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const notificationTransactionResponse = await transaction.run(mentionedCypher, { - id, - idsOfUsers, - reason, - }) - return notificationTransactionResponse.records.map((record) => record.get('notification')) - }) try { - return await writeTxResultPromise - } catch (error) { - throw new Error(error) + return await session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(mentionedCypher, { + id, + idsOfUsers, + reason, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) } finally { - session.close() + await session.close() } } const notifyUsersOfComment = async (label, commentId, reason, context) => { await validateNotifyUsers(label, reason) const session = context.driver.session() - const writeTxResultPromise = await session.writeTransaction(async (transaction) => { - const notificationTransactionResponse = await transaction.run( - ` - MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User) - WHERE NOT (observingUser)-[:BLOCKED]-(commenter) - AND NOT (observingUser)-[:MUTED]->(commenter) - AND NOT observingUser.id = $userId - OPTIONAL MATCH (observingUser)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) - WITH observingUser, emailAddress, post, comment, commenter - MATCH (postAuthor:User)-[:WROTE]->(post) - MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) - SET notification.read = FALSE - SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) - SET notification.updatedAt = toString(datetime()) - WITH notification, observingUser, emailAddress.email as email, post, commenter, postAuthor, - comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource - RETURN notification { - .*, - from: finalResource, - to: properties(observingUser), - email: email, - relatedUser: properties(commenter) - } - `, - { - commentId, - reason, - userId: context.user.id, - }, - ) - return notificationTransactionResponse.records.map((record) => record.get('notification')) - }) try { - return await writeTxResultPromise + return await session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run( + ` + MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User) + WHERE NOT (observingUser)-[:BLOCKED]-(commenter) + AND NOT (observingUser)-[:MUTED]->(commenter) + AND NOT observingUser.id = $userId + OPTIONAL MATCH (observingUser)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH observingUser, emailAddress, post, comment, commenter + MATCH (postAuthor:User)-[:WROTE]->(post) + MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) + SET notification.read = FALSE + SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) + SET notification.updatedAt = toString(datetime()) + WITH notification, observingUser, emailAddress.email as email, post, commenter, postAuthor, + comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource + RETURN notification { + .*, + from: finalResource, + to: properties(observingUser), + email: email, + relatedUser: properties(commenter) + } + `, + { + commentId, + reason, + userId: context.user.id, + }, + ) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) } finally { - session.close() + await session.close() } } @@ -447,30 +431,29 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => // Find Recipient const session = context.driver.session() - const messageRecipient = session.readTransaction(async (transaction) => { - const messageRecipientCypher = ` - MATCH (senderUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) - MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) - WHERE NOT recipientUser.id = $currentUserId - AND NOT (recipientUser)-[:BLOCKED]-(senderUser) - AND NOT (recipientUser)-[:MUTED]->(senderUser) - RETURN senderUser {.*}, recipientUser {.*}, emailAddress {.email} - ` - const txResponse = await transaction.run(messageRecipientCypher, { - currentUserId, - roomId, - }) - - return { - senderUser: await txResponse.records.map((record) => record.get('senderUser'))[0], - recipientUser: await txResponse.records.map((record) => record.get('recipientUser'))[0], - email: await txResponse.records.map((record) => record.get('emailAddress'))[0]?.email, - } - }) - try { - // Execute Query - const { senderUser, recipientUser, email } = await messageRecipient + const { senderUser, recipientUser, email } = await session.readTransaction( + async (transaction) => { + const messageRecipientCypher = ` + MATCH (senderUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) + MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WHERE NOT recipientUser.id = $currentUserId + AND NOT (recipientUser)-[:BLOCKED]-(senderUser) + AND NOT (recipientUser)-[:MUTED]->(senderUser) + RETURN senderUser {.*}, recipientUser {.*}, emailAddress {.email} + ` + const txResponse = await transaction.run(messageRecipientCypher, { + currentUserId, + roomId, + }) + + return { + senderUser: txResponse.records.map((record) => record.get('senderUser'))[0], + recipientUser: txResponse.records.map((record) => record.get('recipientUser'))[0], + email: txResponse.records.map((record) => record.get('emailAddress'))[0]?.email, + } + }, + ) if (recipientUser) { // send subscriptions @@ -493,10 +476,8 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => // Return resolver result to client return message - } catch (error) { - throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/middleware/userInteractions.ts b/backend/src/middleware/userInteractions.ts index bb850a650..469abf318 100644 --- a/backend/src/middleware/userInteractions.ts +++ b/backend/src/middleware/userInteractions.ts @@ -31,7 +31,7 @@ const setPostCounter = async (postId, relation, context) => { return txc.run(createRelatedCypher(relation), { currentUser, postId }) }) } finally { - session.close() + await session.close() } } diff --git a/backend/src/middleware/validation/validationMiddleware.ts b/backend/src/middleware/validation/validationMiddleware.ts index 75f8f5d09..80da928b7 100644 --- a/backend/src/middleware/validation/validationMiddleware.ts +++ b/backend/src/middleware/validation/validationMiddleware.ts @@ -38,7 +38,7 @@ const validateCreateComment = async (resolve, root, args, context, info) => { return resolve(root, args, context, info) } } finally { - session.close() + await session.close() } } @@ -64,28 +64,27 @@ const validateReview = async (resolve, root, args, context, info) => { const { user, driver } = context if (resourceId === user.id) throw new Error('You cannot review yourself!') const session = driver.session() - const reportReadTxPromise = session.readTransaction(async (transaction) => { - const validateReviewTransactionResponse = await transaction.run( - ` - MATCH (resource {id: $resourceId}) - WHERE resource:User OR resource:Post OR resource:Comment - OPTIONAL MATCH (:User)-[filed:FILED]->(:Report {closed: false})-[:BELONGS_TO]->(resource) - OPTIONAL MATCH (resource)<-[:WROTE]-(author:User) - RETURN [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'User']][0] AS label, author, filed - `, - { - resourceId, - submitterId: user.id, - }, - ) - return validateReviewTransactionResponse.records.map((record) => ({ - label: record.get('label'), - author: record.get('author'), - filed: record.get('filed'), - })) - }) try { - const txResult = await reportReadTxPromise + const txResult = await session.readTransaction(async (transaction) => { + const validateReviewTransactionResponse = await transaction.run( + ` + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Post OR resource:Comment + OPTIONAL MATCH (:User)-[filed:FILED]->(:Report {closed: false})-[:BELONGS_TO]->(resource) + OPTIONAL MATCH (resource)<-[:WROTE]-(author:User) + RETURN [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'User']][0] AS label, author, filed + `, + { + resourceId, + submitterId: user.id, + }, + ) + return validateReviewTransactionResponse.records.map((record) => ({ + label: record.get('label'), + author: record.get('author'), + filed: record.get('filed'), + })) + }) existingReportedResource = txResult if (!existingReportedResource?.length) throw new Error(`Resource not found or is not a Post|Comment|User!`) @@ -101,7 +100,7 @@ const validateReview = async (resolve, root, args, context, info) => { if (authorId && authorId === user.id) throw new Error(`You cannot review your own ${existingReportedResource.label}!`) } finally { - session.close() + await session.close() } return resolve(root, args, context, info) diff --git a/backend/src/server.ts b/backend/src/server.ts index 5826a746c..f1bdb60c3 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -15,6 +15,7 @@ import helmet from 'helmet' import CONFIG from './config' import { context, getContext } from './context' import schema from './graphql/schema' +import logger from './logger' import middleware from './middleware' import type { ApolloServerExpressConfig } from 'apollo-server-express' @@ -24,8 +25,12 @@ const createServer = (options?: ApolloServerExpressConfig) => { context, schema: middleware(schema), subscriptions: { + keepAlive: 10000, onConnect: (connectionParams) => getContext()(connectionParams as { headers: { authorization?: string } }), + onDisconnect: () => { + logger.debug('WebSocket client disconnected') + }, }, debug: !!CONFIG.DEBUG, uploads: false, diff --git a/backend/src/uploads/s3Service.ts b/backend/src/uploads/s3Service.ts index d2e138fee..bf82d6238 100644 --- a/backend/src/uploads/s3Service.ts +++ b/backend/src/uploads/s3Service.ts @@ -5,11 +5,22 @@ import type { S3Config } from '@config/index' import { FileUploadCallback, FileDeleteCallback } from './types' -export const s3Service = (config: S3Config, prefix: string) => { - const { AWS_BUCKET: Bucket } = config +let cachedClient: S3Client | null = null +let cachedConfig: S3Config | null = null +const getS3Client = (config: S3Config): S3Client => { + if (cachedClient) { + if ( + cachedConfig?.AWS_ENDPOINT !== config.AWS_ENDPOINT || + cachedConfig?.AWS_ACCESS_KEY_ID !== config.AWS_ACCESS_KEY_ID || + cachedConfig?.AWS_SECRET_ACCESS_KEY !== config.AWS_SECRET_ACCESS_KEY + ) { + throw new Error('S3Client singleton was created with different credentials') + } + return cachedClient + } const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = config - const s3 = new S3Client({ + cachedClient = new S3Client({ credentials: { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY, @@ -17,6 +28,13 @@ export const s3Service = (config: S3Config, prefix: string) => { endpoint: AWS_ENDPOINT, forcePathStyle: true, }) + cachedConfig = config + return cachedClient +} + +export const s3Service = (config: S3Config, prefix: string) => { + const { AWS_BUCKET: Bucket } = config + const s3 = getS3Client(config) const uploadFile: FileUploadCallback = async ({ createReadStream, uniqueFilename, mimetype }) => { const s3Location = prefix.length > 0 ? `${prefix}/${uniqueFilename}` : uniqueFilename