From 132c12a7d3b24db62c4bb1816a890c76edc29425 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 2 Dec 2019 18:11:25 +0100 Subject: [PATCH] Close neo4j driver sessions We had this error in our neo4j pod recently: ``` 2019-12-02 08:29:42.680+0000 ERROR Unable to schedule bolt session 'bolt-1018230' for execution since there are no available threads to serve it at the moment. You can retry at a later time or consider increasing max thread pool size for bolt connector(s). 2019-12-02 08:29:42.680+0000 ERROR Unable to schedule bolt session 'bolt-1018224' for execution since there are no available threads to serve it at the moment. You can retry at a later time or consider increasing max thread pool size for bolt connector(s). 2019-12-02 08:29:42.681+0000 ERROR Unable to schedule bolt session 'bolt-1018352' for execution since there are no available threads to serve it at the moment. You can retry at a later time or consider increasing max thread pool size for bolt connector(s). 2019-12-02 08:29:42.682+0000 ERROR Unable to schedule bolt session 'bolt-1018243' for execution since there are no available threads to serve it at the moment. You can retry at a later time or consider increasing max thread pool size for bolt connector(s). ``` Apparently the default is 400 threads. So we must have a leak somewhere. --- backend/src/jwt/decode.js | 12 +- .../middleware/hashtags/hashtagsMiddleware.js | 21 +- .../notifications/notificationsMiddleware.js | 45 ++-- .../src/middleware/permissionsMiddleware.js | 2 +- backend/src/middleware/sluggifyMiddleware.js | 13 +- .../validation/validationMiddleware.js | 64 +++--- backend/src/schema/resolvers/comments.js | 51 +++-- backend/src/schema/resolvers/donations.js | 2 +- .../resolvers/helpers/createPasswordReset.js | 4 +- backend/src/schema/resolvers/moderation.js | 34 +-- backend/src/schema/resolvers/notifications.js | 13 +- backend/src/schema/resolvers/passwordReset.js | 25 ++- .../schema/resolvers/passwordReset.spec.js | 11 +- backend/src/schema/resolvers/posts.js | 195 +++++++++--------- backend/src/schema/resolvers/reports.js | 40 ++-- backend/src/schema/resolvers/shout.js | 61 +++--- backend/src/schema/resolvers/statistics.js | 2 +- .../src/schema/resolvers/user_management.js | 41 ++-- backend/src/seed/factories/index.js | 2 +- 19 files changed, 350 insertions(+), 288 deletions(-) diff --git a/backend/src/jwt/decode.js b/backend/src/jwt/decode.js index 842f8f537..5b7881d20 100644 --- a/backend/src/jwt/decode.js +++ b/backend/src/jwt/decode.js @@ -11,15 +11,21 @@ export default async (driver, authorizationHeader) => { } catch (err) { return null } - const session = driver.session() const query = ` MATCH (user:User {id: $id, deleted: false, disabled: false }) SET user.lastActiveAt = toString(datetime()) RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId} LIMIT 1 ` - const result = await session.run(query, { id }) - session.close() + const session = driver.session() + let result + + try { + result = await session.run(query, { id }) + } finally { + session.close() + } + const [currentUser] = await result.records.map(record => { return record.get('user') }) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.js b/backend/src/middleware/hashtags/hashtagsMiddleware.js index c9156398d..53a8fed20 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.js @@ -3,7 +3,6 @@ import extractHashtags from '../hashtags/extractHashtags' const updateHashtagsOfPost = async (postId, hashtags, context) => { if (!hashtags.length) return - const session = context.driver.session() // We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement // functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted // and no new Hashtags and relations will be created. @@ -19,14 +18,18 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => { MERGE (p)-[:TAGGED]->(t) RETURN p, t ` - await session.run(cypherDeletePreviousRelations, { - postId, - }) - await session.run(cypherCreateNewTagsAndRelations, { - postId, - hashtags, - }) - session.close() + const session = context.driver.session() + try { + await session.run(cypherDeletePreviousRelations, { + postId, + }) + await session.run(cypherCreateNewTagsAndRelations, { + postId, + hashtags, + }) + } finally { + session.close() + } } const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index 718f0b1e4..ac199a67d 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -1,15 +1,19 @@ import extractMentionedUsers from './mentions/extractMentionedUsers' const postAuthorOfComment = async (comment, { context }) => { - const session = context.driver.session() const cypherFindUser = ` MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) RETURN user { .id } ` - const result = await session.run(cypherFindUser, { - commentId: comment.id, - }) - session.close() + const session = context.driver.session() + let result + try { + result = await session.run(cypherFindUser, { + commentId: comment.id, + }) + } finally { + session.close() + } const [postAuthor] = await result.records.map(record => { return record.get('user') }) @@ -31,7 +35,6 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { throw new Error('Notification does not fit the reason!') } - const session = context.driver.session() let cypher switch (reason) { case 'mentioned_in_post': { @@ -85,12 +88,16 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { break } } - await session.run(cypher, { - id, - idsOfUsers, - reason, - }) - session.close() + const session = context.driver.session() + try { + await session.run(cypher, { + id, + idsOfUsers, + reason, + }) + } finally { + session.close() + } } const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { @@ -123,15 +130,19 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo) if (comment) { - const session = context.driver.session() const cypherFindUser = ` MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) RETURN user { .id } ` - const result = await session.run(cypherFindUser, { - commentId: comment.id, - }) - session.close() + const session = context.driver.session() + let result + try { + result = await session.run(cypherFindUser, { + commentId: comment.id, + }) + } finally { + session.close() + } const [postAuthor] = await result.records.map(record => { return record.get('user') }) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index e729123c9..f3c8ca65e 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -45,8 +45,8 @@ const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { if (!user) return false - const session = driver.session() const { id: resourceId } = args + const session = driver.session() try { const result = await session.run( ` diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index fed9b4da7..cda3fd335 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -3,11 +3,14 @@ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { return async slug => { const session = context.driver.session() - const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, { - slug, - }) - session.close() - return response.records.length === 0 + try { + const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, { + slug, + }) + return response.records.length === 0 + } finally { + session.close() + } } } diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index bd4805ed8..024e594bf 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -13,23 +13,26 @@ const validateCommentCreation = async (resolve, root, args, context, info) => { throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) } const session = context.driver.session() - const postQueryRes = await session.run( - ` + try { + const postQueryRes = await session.run( + ` MATCH (post:Post {id: $postId}) RETURN post`, - { - postId, - }, - ) - session.close() - const [post] = postQueryRes.records.map(record => { - return record.get('post') - }) + { + postId, + }, + ) + const [post] = postQueryRes.records.map(record => { + return record.get('post') + }) - if (!post) { - throw new UserInputError(NO_POST_ERR_MESSAGE) - } else { - return resolve(root, args, context, info) + if (!post) { + throw new UserInputError(NO_POST_ERR_MESSAGE) + } else { + return resolve(root, args, context, info) + } + } finally { + session.close() } } @@ -62,25 +65,28 @@ const validateReport = async (resolve, root, args, context, info) => { const { user, driver } = context if (resourceId === user.id) throw new Error('You cannot report yourself!') const session = driver.session() - const reportQueryRes = await session.run( - ` + try { + const reportQueryRes = await session.run( + ` MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) RETURN labels(resource)[0] as label `, - { - resourceId, - submitterId: user.id, - }, - ) - session.close() - const [existingReportedResource] = reportQueryRes.records.map(record => { - return { - label: record.get('label'), - } - }) + { + resourceId, + submitterId: user.id, + }, + ) + const [existingReportedResource] = reportQueryRes.records.map(record => { + return { + label: record.get('label'), + } + }) - if (existingReportedResource) throw new Error(`${existingReportedResource.label}`) - return resolve(root, args, context, info) + if (existingReportedResource) throw new Error(`${existingReportedResource.label}`) + return resolve(root, args, context, info) + } finally { + session.close() + } } export default { diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index e0b69b153..20869a73a 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -13,7 +13,8 @@ export default { params.id = params.id || uuid() const session = context.driver.session() - const createCommentCypher = ` + try { + const createCommentCypher = ` MATCH (post:Post {id: $postId}) MATCH (author:User {id: $userId}) WITH post, author @@ -23,45 +24,53 @@ export default { MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author) RETURN comment ` - const transactionRes = await session.run(createCommentCypher, { - userId: context.user.id, - postId, - params, - }) - session.close() + const transactionRes = await session.run(createCommentCypher, { + userId: context.user.id, + postId, + params, + }) - const [comment] = transactionRes.records.map(record => record.get('comment').properties) + const [comment] = transactionRes.records.map(record => record.get('comment').properties) - return comment + return comment + } finally { + session.close() + } }, UpdateComment: async (_parent, params, context, _resolveInfo) => { const session = context.driver.session() - const updateCommentCypher = ` + try { + const updateCommentCypher = ` MATCH (comment:Comment {id: $params.id}) SET comment += $params SET comment.updatedAt = toString(datetime()) RETURN comment ` - const transactionRes = await session.run(updateCommentCypher, { params }) - session.close() - const [comment] = transactionRes.records.map(record => record.get('comment').properties) - return comment + const transactionRes = await session.run(updateCommentCypher, { params }) + const [comment] = transactionRes.records.map(record => record.get('comment').properties) + return comment + } finally { + session.close() + } }, DeleteComment: async (_parent, args, context, _resolveInfo) => { const session = context.driver.session() - const transactionRes = await session.run( - ` + try { + const transactionRes = await session.run( + ` MATCH (comment:Comment {id: $commentId}) SET comment.deleted = TRUE SET comment.content = 'UNAVAILABLE' SET comment.contentExcerpt = 'UNAVAILABLE' RETURN comment `, - { commentId: args.id }, - ) - session.close() - const [comment] = transactionRes.records.map(record => record.get('comment').properties) - return comment + { commentId: args.id }, + ) + const [comment] = transactionRes.records.map(record => record.get('comment').properties) + return comment + } finally { + session.close() + } }, }, Comment: { diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js index 88149077d..3052ff13d 100644 --- a/backend/src/schema/resolvers/donations.js +++ b/backend/src/schema/resolvers/donations.js @@ -2,8 +2,8 @@ export default { Mutation: { UpdateDonations: async (_parent, params, context, _resolveInfo) => { const { driver } = context - const session = driver.session() let donations + const session = driver.session() const writeTxResultPromise = session.writeTransaction(async txc => { const updateDonationsTransactionResponse = await txc.run( ` diff --git a/backend/src/schema/resolvers/helpers/createPasswordReset.js b/backend/src/schema/resolvers/helpers/createPasswordReset.js index d73cfaa81..41214b501 100644 --- a/backend/src/schema/resolvers/helpers/createPasswordReset.js +++ b/backend/src/schema/resolvers/helpers/createPasswordReset.js @@ -4,7 +4,6 @@ export default async function createPasswordReset(options) { const { driver, nonce, email, issuedAt = new Date() } = options const normalizedEmail = normalizeEmail(email) const session = driver.session() - let response = {} try { const cypher = ` MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) @@ -23,9 +22,8 @@ export default async function createPasswordReset(options) { const { name } = record.get('u').properties return { email, nonce, name } }) - response = records[0] || {} + return records[0] || {} } finally { session.close() } - return response } diff --git a/backend/src/schema/resolvers/moderation.js b/backend/src/schema/resolvers/moderation.js index d61df7545..de756b7b2 100644 --- a/backend/src/schema/resolvers/moderation.js +++ b/backend/src/schema/resolvers/moderation.js @@ -12,13 +12,16 @@ export default { RETURN resource {.id} ` const session = driver.session() - const res = await session.run(cypher, { id, userId }) - session.close() - const [resource] = res.records.map(record => { - return record.get('resource') - }) - if (!resource) return null - return resource.id + try { + const res = await session.run(cypher, { id, userId }) + const [resource] = res.records.map(record => { + return record.get('resource') + }) + if (!resource) return null + return resource.id + } finally { + session.close() + } }, enable: async (object, params, { user, driver }) => { const { id } = params @@ -29,13 +32,16 @@ export default { RETURN resource {.id} ` const session = driver.session() - const res = await session.run(cypher, { id }) - session.close() - const [resource] = res.records.map(record => { - return record.get('resource') - }) - if (!resource) return null - return resource.id + try { + const res = await session.run(cypher, { id }) + const [resource] = res.records.map(record => { + return record.get('resource') + }) + if (!resource) return null + return resource.id + } finally { + session.close() + } }, }, } diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 9e6f5c91a..7f9c52e1e 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -18,7 +18,7 @@ export default { notifications: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() - let notifications, whereClause, orderByClause + let whereClause, orderByClause switch (args.read) { case true: @@ -42,27 +42,25 @@ export default { } const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : '' const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : '' - try { - const cypher = ` + const cypher = ` MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) ${whereClause} RETURN resource, notification, user ${orderByClause} ${offset} ${limit} ` + try { const result = await session.run(cypher, { id: currentUser.id }) - notifications = await result.records.map(transformReturnType) + return result.records.map(transformReturnType) } finally { session.close() } - return notifications }, }, Mutation: { markAsRead: async (parent, args, context, resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() - let notification try { const cypher = ` MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id}) @@ -71,11 +69,10 @@ export default { ` const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id }) const notifications = await result.records.map(transformReturnType) - notification = notifications[0] + return notifications[0] } finally { session.close() } - return notification }, }, NOTIFIED: { diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index 7c0d9e747..dfbfe8183 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -9,7 +9,6 @@ export default { return createPasswordReset({ driver, nonce, email }) }, resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { - const session = driver.session() const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) @@ -21,16 +20,20 @@ export default { SET u.encryptedPassword = $encryptedNewPassword RETURN pr ` - const transactionRes = await session.run(cypher, { - stillValid, - email, - nonce, - encryptedNewPassword, - }) - const [reset] = transactionRes.records.map(record => record.get('pr')) - const response = !!(reset && reset.properties.usedAt) - session.close() - return response + const session = driver.session() + try { + const transactionRes = await session.run(cypher, { + stillValid, + email, + nonce, + encryptedNewPassword, + }) + const [reset] = transactionRes.records.map(record => record.get('pr')) + const response = !!(reset && reset.properties.usedAt) + return response + } finally { + session.close() + } }, }, } diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 8b36b8c85..97aa6a020 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -15,10 +15,13 @@ let variables const getAllPasswordResets = async () => { const session = driver.session() - const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r') - const resets = transactionRes.records.map(record => record.get('r')) - session.close() - return resets + try { + const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r') + const resets = transactionRes.records.map(record => record.get('r')) + return resets + } finally { + session.close() + } } beforeEach(() => { diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index ee6a82d42..1322b10e4 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -54,37 +54,41 @@ export default { return neo4jgraphql(object, params, context, resolveInfo) }, PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { postId, data } = params - const transactionRes = await session.run( - `MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() RETURN COUNT(DISTINCT emoted) as emotionsCount `, - { postId, data }, - ) - session.close() + { postId, data }, + ) - const [emotionsCount] = transactionRes.records.map(record => { - return record.get('emotionsCount').low - }) - - return emotionsCount + const [emotionsCount] = transactionRes.records.map(record => { + return record.get('emotionsCount').low + }) + return emotionsCount + } finally { + session.close() + } }, PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { postId } = params - const transactionRes = await session.run( - `MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) RETURN collect(emoted.emotion) as emotion`, - { userId: context.user.id, postId }, - ) + { userId: context.user.id, postId }, + ) - session.close() - - const [emotions] = transactionRes.records.map(record => { - return record.get('emotion') - }) - return emotions + const [emotions] = transactionRes.records.map(record => { + return record.get('emotion') + }) + return emotions + } finally { + session.close() + } }, }, Mutation: { @@ -93,8 +97,6 @@ export default { delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params.id = params.id || uuid() - let post - const createPostCypher = `CREATE (post:Post {params}) SET post.createdAt = toString(datetime()) SET post.updatedAt = toString(datetime()) @@ -113,7 +115,7 @@ export default { try { const transactionRes = await session.run(createPostCypher, createPostVariables) const posts = transactionRes.records.map(record => record.get('post').properties) - post = posts[0] + return posts[0] } catch (e) { if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') throw new UserInputError('Post with this slug already exists!') @@ -121,55 +123,55 @@ export default { } finally { session.close() } - - return post }, UpdatePost: async (_parent, params, context, _resolveInfo) => { const { categoryIds } = params delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) - const session = context.driver.session() let updatePostCypher = `MATCH (post:Post {id: $params.id}) SET post += $params SET post.updatedAt = toString(datetime()) WITH post ` - if (categoryIds && categoryIds.length) { - const cypherDeletePreviousRelations = ` + const session = context.driver.session() + try { + if (categoryIds && categoryIds.length) { + const cypherDeletePreviousRelations = ` MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category) DELETE previousRelations RETURN post, category ` - await session.run(cypherDeletePreviousRelations, { params }) + await session.run(cypherDeletePreviousRelations, { params }) - updatePostCypher += ` + updatePostCypher += ` UNWIND $categoryIds AS categoryId MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category) WITH post ` + } + + updatePostCypher += `RETURN post` + const updatePostVariables = { categoryIds, params } + + const transactionRes = await session.run(updatePostCypher, updatePostVariables) + const [post] = transactionRes.records.map(record => { + return record.get('post').properties + }) + return post + } finally { + session.close() } - - updatePostCypher += `RETURN post` - const updatePostVariables = { categoryIds, params } - - const transactionRes = await session.run(updatePostCypher, updatePostVariables) - const [post] = transactionRes.records.map(record => { - return record.get('post').properties - }) - - session.close() - - return post }, DeletePost: async (object, args, context, resolveInfo) => { const session = context.driver.session() - // we cannot set slug to 'UNAVAILABE' because of unique constraints - const transactionRes = await session.run( - ` + try { + // we cannot set slug to 'UNAVAILABE' because of unique constraints + const transactionRes = await session.run( + ` MATCH (post:Post {id: $postId}) OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment) SET post.deleted = TRUE @@ -180,51 +182,60 @@ export default { REMOVE post.image RETURN post `, - { postId: args.id }, - ) - session.close() - const [post] = transactionRes.records.map(record => record.get('post').properties) - return post + { postId: args.id }, + ) + const [post] = transactionRes.records.map(record => record.get('post').properties) + return post + } finally { + session.close() + } }, AddPostEmotions: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { to, data } = params const { user } = context - const transactionRes = await session.run( - `MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id}) + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id}) MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo) RETURN userFrom, postTo, emotedRelation`, - { user, to, data }, - ) - session.close() - const [emoted] = transactionRes.records.map(record => { - return { - from: { ...record.get('userFrom').properties }, - to: { ...record.get('postTo').properties }, - ...record.get('emotedRelation').properties, - } - }) - return emoted + { user, to, data }, + ) + + const [emoted] = transactionRes.records.map(record => { + return { + from: { ...record.get('userFrom').properties }, + to: { ...record.get('postTo').properties }, + ...record.get('emotedRelation').properties, + } + }) + return emoted + } finally { + session.close() + } }, RemovePostEmotions: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { to, data } = params const { id: from } = context.user - const transactionRes = await session.run( - `MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) DELETE emotedRelation RETURN userFrom, postTo`, - { from, to, data }, - ) - session.close() - const [emoted] = transactionRes.records.map(record => { - return { - from: { ...record.get('userFrom').properties }, - to: { ...record.get('postTo').properties }, - emotion: data.emotion, - } - }) - return emoted + { from, to, data }, + ) + const [emoted] = transactionRes.records.map(record => { + return { + from: { ...record.get('userFrom').properties }, + to: { ...record.get('postTo').properties }, + emotion: data.emotion, + } + }) + return emoted + } finally { + session.close() + } }, pinPost: async (_parent, params, context, _resolveInfo) => { let pinnedPostWithNestedAttributes @@ -242,25 +253,25 @@ export default { ) return deletePreviousRelationsResponse.records.map(record => record.get('post').properties) }) - await writeTxResultPromise + try { + await writeTxResultPromise - writeTxResultPromise = session.writeTransaction(async transaction => { - const pinPostTransactionResponse = await transaction.run( - ` + writeTxResultPromise = session.writeTransaction(async transaction => { + const pinPostTransactionResponse = await transaction.run( + ` MATCH (user:User {id: $userId}) WHERE user.role = 'admin' MATCH (post:Post {id: $params.id}) MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post) SET post.pinned = true RETURN post, pinned.createdAt as pinnedAt `, - { userId, params }, - ) - return pinPostTransactionResponse.records.map(record => ({ - pinnedPost: record.get('post').properties, - pinnedAt: record.get('pinnedAt'), - })) - }) - try { + { userId, params }, + ) + return pinPostTransactionResponse.records.map(record => ({ + pinnedPost: record.get('post').properties, + pinnedAt: record.get('pinnedAt'), + })) + }) const [transactionResult] = await writeTxResultPromise const { pinnedPost, pinnedAt } = transactionResult pinnedPostWithNestedAttributes = { diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 083c94362..8e12f1dba 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -5,31 +5,31 @@ export default { const { resourceId, reasonCategory, reasonDescription } = params const { driver, user } = context const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async txc => { - const reportRelationshipTransactionResponse = await txc.run( - ` + try { + const writeTxResultPromise = session.writeTransaction(async txc => { + const reportRelationshipTransactionResponse = await txc.run( + ` MATCH (submitter:User {id: $submitterId}) MATCH (resource {id: $resourceId}) WHERE resource:User OR resource:Comment OR resource:Post CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) RETURN report, submitter, resource, labels(resource)[0] as type `, - { - resourceId, - submitterId: user.id, - createdAt: new Date().toISOString(), - reasonCategory, - reasonDescription, - }, - ) - return reportRelationshipTransactionResponse.records.map(record => ({ - report: record.get('report'), - submitter: record.get('submitter'), - resource: record.get('resource').properties, - type: record.get('type'), - })) - }) - try { + { + resourceId, + submitterId: user.id, + createdAt: new Date().toISOString(), + reasonCategory, + reasonDescription, + }, + ) + return reportRelationshipTransactionResponse.records.map(record => ({ + report: record.get('report'), + submitter: record.get('submitter'), + resource: record.get('resource').properties, + type: record.get('type'), + })) + }) const txResult = await writeTxResultPromise if (!txResult[0]) return null const { report, submitter, resource, type } = txResult[0] @@ -61,7 +61,6 @@ export default { Query: { reports: async (_parent, params, context, _resolveInfo) => { const { driver } = context - const session = driver.session() let response let orderByClause switch (params.orderBy) { @@ -74,6 +73,7 @@ export default { default: orderByClause = '' } + const session = driver.session() try { const cypher = ` MATCH (submitter:User)-[report:REPORTED]->(resource) diff --git a/backend/src/schema/resolvers/shout.js b/backend/src/schema/resolvers/shout.js index 05de9b103..ada1172a4 100644 --- a/backend/src/schema/resolvers/shout.js +++ b/backend/src/schema/resolvers/shout.js @@ -4,48 +4,51 @@ export default { const { id, type } = params const session = context.driver.session() - const transactionRes = await session.run( - `MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) + try { + const transactionRes = await session.run( + `MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) WHERE $type IN labels(node) AND NOT userWritten.id = $userId MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node) RETURN COUNT(relation) > 0 as isShouted`, - { - id, - type, - userId: context.user.id, - }, - ) + { + id, + type, + userId: context.user.id, + }, + ) - const [isShouted] = transactionRes.records.map(record => { - return record.get('isShouted') - }) + const [isShouted] = transactionRes.records.map(record => { + return record.get('isShouted') + }) - session.close() - - return isShouted + return isShouted + } finally { + session.close() + } }, unshout: async (_object, params, context, _resolveInfo) => { const { id, type } = params const session = context.driver.session() - - const transactionRes = await session.run( - `MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id}) + try { + const transactionRes = await session.run( + `MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id}) WHERE $type IN labels(node) DELETE relation RETURN COUNT(relation) > 0 as isShouted`, - { - id, - type, - userId: context.user.id, - }, - ) - const [isShouted] = transactionRes.records.map(record => { - return record.get('isShouted') - }) - session.close() - - return isShouted + { + id, + type, + userId: context.user.id, + }, + ) + const [isShouted] = transactionRes.records.map(record => { + return record.get('isShouted') + }) + return isShouted + } finally { + session.close() + } }, }, } diff --git a/backend/src/schema/resolvers/statistics.js b/backend/src/schema/resolvers/statistics.js index 7b06f8705..e8745efea 100644 --- a/backend/src/schema/resolvers/statistics.js +++ b/backend/src/schema/resolvers/statistics.js @@ -33,10 +33,10 @@ export default { * Note: invites count is calculated this way because invitation codes are not in use yet */ response.countInvites = response.countEmails - response.countUsers + return response } finally { session.close() } - return response }, }, } diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index 81550d8cf..4c4c3fc90 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -24,29 +24,32 @@ export default { // } email = normalizeEmail(email) const session = driver.session() - const result = await session.run( - ` + try { + const result = await session.run( + ` MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail}) RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1 `, - { userEmail: email }, - ) - session.close() - const [currentUser] = await result.records.map(record => { - return record.get('user') - }) + { userEmail: email }, + ) + const [currentUser] = await result.records.map(record => { + return record.get('user') + }) - if ( - currentUser && - (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && - !currentUser.disabled - ) { - delete currentUser.encryptedPassword - return encode(currentUser) - } else if (currentUser && currentUser.disabled) { - throw new AuthenticationError('Your account has been disabled.') - } else { - throw new AuthenticationError('Incorrect email address or password.') + if ( + currentUser && + (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && + !currentUser.disabled + ) { + delete currentUser.encryptedPassword + return encode(currentUser) + } else if (currentUser && currentUser.disabled) { + throw new AuthenticationError('Your account has been disabled.') + } else { + throw new AuthenticationError('Incorrect email address or password.') + } + } finally { + session.close() } }, changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 5054155fc..1b090530b 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -27,8 +27,8 @@ const factories = { export const cleanDatabase = async (options = {}) => { const { driver = getDriver() } = options - const session = driver.session() const cypher = 'MATCH (n) DETACH DELETE n' + const session = driver.session() try { return await session.run(cypher) } finally {