diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index b094231c09..64ba60f5e5 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -1,9 +1,9 @@ -import { getNeode } from '../../../bootstrap/neo4j' +import log from './databaseLogger' export const undefinedToNullResolver = list => { const resolvers = {} list.forEach(key => { - resolvers[key] = async (parent, params, context, resolveInfo) => { + resolvers[key] = async parent => { return typeof parent[key] === 'undefined' ? null : parent[key] } }) @@ -11,7 +11,6 @@ export const undefinedToNullResolver = list => { } export default function Resolver(type, options = {}) { - const instance = getNeode() const { idAttribute = 'id', undefinedToNull = [], @@ -22,32 +21,49 @@ export default function Resolver(type, options = {}) { } = options const _hasResolver = (resolvers, { key, connection }, { returnType }) => { - return async (parent, params, context, resolveInfo) => { + return async (parent, params, { driver, cypherParams }, resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] - const statement = `MATCH(:${type} {${idAttribute}: {id}})${connection} RETURN related` - const result = await instance.cypher(statement, { id }) - let response = result.records.map(r => r.get('related').properties) - if (returnType === 'object') response = response[0] || null - return response + 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 }) + log(result) + return result.records.map(r => r.get('related')) + }) + try { + let response = await readTxResultPromise + if (returnType === 'object') response = response[0] || null + return response + } finally { + session.close() + } } } const booleanResolver = obj => { const resolvers = {} for (const [key, condition] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { cypherParams }, resolveInfo) => { + resolvers[key] = async (parent, params, { cypherParams, driver }, resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] - const result = await instance.cypher( - ` - ${condition.replace('this', 'this {id: $parent.id}')} as ${key}`, - { - parent, - cypherParams, - }, - ) - const [record] = result.records - return record.get(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 }) + log(result) + const [response] = result.records.map(r => r.get(key)) + return response + }) + try { + return await readTxResultPromise + } finally { + session.close() + } } } return resolvers @@ -56,16 +72,25 @@ export default function Resolver(type, options = {}) { const countResolver = obj => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = async (parent, params, context, resolveInfo) => { + resolvers[key] = async (parent, params, { driver, cypherParams }, resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] - const id = parent[idAttribute] - const statement = ` - MATCH(u:${type} {${idAttribute}: {id}})${connection} - RETURN COUNT(DISTINCT(related)) as count - ` - const result = await instance.cypher(statement, { id }) - const [response] = result.records.map(r => r.get('count').toNumber()) - return response + 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 }) + log(result) + const [response] = result.records.map(r => r.get('count').toNumber()) + return response + }) + try { + return await readTxResultPromise + } finally { + session.close() + } } } return resolvers diff --git a/backend/src/schema/resolvers/helpers/databaseLogger.js b/backend/src/schema/resolvers/helpers/databaseLogger.js new file mode 100644 index 0000000000..1e97b4d723 --- /dev/null +++ b/backend/src/schema/resolvers/helpers/databaseLogger.js @@ -0,0 +1,15 @@ +import Debug from 'debug' +const debugCypher = Debug('human-connection:neo4j:cypher') +const debugStats = Debug('human-connection:neo4j:stats') + +export default function log(response) { + const { statement, counters, resultConsumedAfter, resultAvailableAfter } = response.summary + const { text, parameters } = statement + debugCypher('%s', text) + debugCypher('%o', parameters) + debugStats('%o', counters) + debugStats('%o', { + resultConsumedAfter: resultConsumedAfter.toNumber(), + resultAvailableAfter: resultAvailableAfter.toNumber(), + }) +} diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 7f9c52e1ea..eca12900d8 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -1,3 +1,5 @@ +import log from './helpers/databaseLogger' + const resourceTypes = ['Post', 'Comment'] const transformReturnType = record => { @@ -42,16 +44,29 @@ export default { } const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : '' const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : '' - const cypher = ` - MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) - ${whereClause} - RETURN resource, notification, user - ${orderByClause} - ${offset} ${limit} - ` + + const readTxResultPromise = session.readTransaction(async transaction => { + const notificationsTransactionResponse = await transaction.run( + ` + MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) + ${whereClause} + WITH user, notification, resource, + [(resource)<-[:WROTE]-(author:User) | author {.*}] as authors, + [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] as posts + WITH resource, user, notification, authors, posts, + resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} as finalResource + RETURN notification {.*, from: finalResource, to: properties(user)} + ${orderByClause} + ${offset} ${limit} + `, + { id: currentUser.id }, + ) + log(notificationsTransactionResponse) + return notificationsTransactionResponse.records.map(record => record.get('notification')) + }) try { - const result = await session.run(cypher, { id: currentUser.id }) - return result.records.map(transformReturnType) + const notifications = await readTxResultPromise + return notifications } finally { session.close() } @@ -68,6 +83,7 @@ export default { RETURN resource, notification, user ` const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id }) + log(result) const notifications = await result.records.map(transformReturnType) return notifications[0] } finally { diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 24b8280bd6..89bbd2528a 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -184,6 +184,7 @@ describe('given some notifications', () => { data: { notifications: expect.arrayContaining(expected), }, + errors: undefined, }) }) }) @@ -233,7 +234,10 @@ describe('given some notifications', () => { ` await expect( mutate({ mutation: deletePostMutation, variables: { id: 'p3' } }), - ).resolves.toMatchObject({ data: { DeletePost: { id: 'p3', deleted: true } } }) + ).resolves.toMatchObject({ + data: { DeletePost: { id: 'p3', deleted: true } }, + errors: undefined, + }) authenticatedUser = await user.toJson() } @@ -242,11 +246,12 @@ describe('given some notifications', () => { query({ query: notificationQuery, variables: { ...variables, read: false } }), ).resolves.toMatchObject({ data: { notifications: [expect.any(Object), expect.any(Object)] }, + errors: undefined, }) await deletePostAction() await expect( query({ query: notificationQuery, variables: { ...variables, read: false } }), - ).resolves.toMatchObject({ data: { notifications: [] } }) + ).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined }) }) }) }) diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index a1d98bb415..0565c4d8a2 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -1,3 +1,5 @@ +import log from './helpers/databaseLogger' + const transformReturnType = record => { return { ...record.get('report').properties, @@ -11,12 +13,11 @@ const transformReturnType = record => { export default { Mutation: { fileReport: async (_parent, params, context, _resolveInfo) => { - let createdRelationshipWithNestedAttributes const { resourceId, reasonCategory, reasonDescription } = params const { driver, user } = context const session = driver.session() - const reportWriteTxResultPromise = session.writeTransaction(async txc => { - const reportTransactionResponse = await txc.run( + const reportWriteTxResultPromise = session.writeTransaction(async transaction => { + const reportTransactionResponse = await transaction.run( ` MATCH (submitter:User {id: $submitterId}) MATCH (resource {id: $resourceId}) @@ -36,23 +37,23 @@ export default { reasonDescription, }, ) + log(reportTransactionResponse) return reportTransactionResponse.records.map(transformReturnType) }) try { - const txResult = await reportWriteTxResultPromise - if (!txResult[0]) return null - createdRelationshipWithNestedAttributes = txResult[0] + const [createdRelationshipWithNestedAttributes] = await reportWriteTxResultPromise + if (!createdRelationshipWithNestedAttributes) return null + return createdRelationshipWithNestedAttributes } finally { session.close() } - return createdRelationshipWithNestedAttributes }, }, Query: { reports: async (_parent, params, context, _resolveInfo) => { const { driver } = context const session = driver.session() - let reports, orderByClause, filterClause + let orderByClause, filterClause switch (params.orderBy) { case 'createdAt_asc': orderByClause = 'ORDER BY report.createdAt ASC' @@ -81,8 +82,8 @@ export default { params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : '' const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : '' - const reportReadTxPromise = session.readTransaction(async tx => { - const allReportsTransactionResponse = await tx.run( + const reportReadTxPromise = session.readTransaction(async transaction => { + const allReportsTransactionResponse = await transaction.run( ` MATCH (report:Report)-[:BELONGS_TO]->(resource) WHERE (resource:User OR resource:Post OR resource:Comment) @@ -100,16 +101,15 @@ export default { ${offset} ${limit} `, ) + log(allReportsTransactionResponse) return allReportsTransactionResponse.records.map(record => record.get('report')) }) try { - const txResult = await reportReadTxPromise - if (!txResult[0]) return null - reports = txResult + const reports = await reportReadTxPromise + return reports } finally { session.close() } - return reports }, }, Report: { @@ -118,23 +118,23 @@ export default { const session = context.driver.session() const { id } = parent let filed - const readTxPromise = session.readTransaction(async tx => { - const allReportsTransactionResponse = await tx.run( + const readTxPromise = session.readTransaction(async transaction => { + const filedReportsTransactionResponse = await transaction.run( ` - MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) - RETURN filed, submitter + MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) + RETURN filed, submitter `, { id }, ) - return allReportsTransactionResponse.records.map(record => ({ + log(filedReportsTransactionResponse) + return filedReportsTransactionResponse.records.map(record => ({ submitter: record.get('submitter').properties, filed: record.get('filed').properties, })) }) try { - const txResult = await readTxPromise - if (!txResult[0]) return null - filed = txResult.map(reportedRecord => { + const filedReports = await readTxPromise + filed = filedReports.map(reportedRecord => { const { submitter, filed } = reportedRecord const relationshipWithNestedAttributes = { ...filed, @@ -152,8 +152,8 @@ export default { const session = context.driver.session() const { id } = parent let reviewed - const readTxPromise = session.readTransaction(async tx => { - const allReportsTransactionResponse = await tx.run( + const readTxPromise = session.readTransaction(async transaction => { + const reviewedReportsTransactionResponse = await transaction.run( ` MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) RETURN moderator, review @@ -161,14 +161,15 @@ export default { `, { id }, ) - return allReportsTransactionResponse.records.map(record => ({ + log(reviewedReportsTransactionResponse) + return reviewedReportsTransactionResponse.records.map(record => ({ review: record.get('review').properties, moderator: record.get('moderator').properties, })) }) try { - const txResult = await readTxPromise - reviewed = txResult.map(reportedRecord => { + const reviewedReports = await readTxPromise + reviewed = reviewedReports.map(reportedRecord => { const { review, moderator } = reportedRecord const relationshipWithNestedAttributes = { ...review, diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index cd8b619859..8b1bb925d9 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -21,7 +21,6 @@ describe('file a report on a resource', () => { id createdAt updatedAt - disable closed rule resource { @@ -489,7 +488,6 @@ describe('file a report on a resource', () => { id createdAt updatedAt - disable closed resource { __typename @@ -624,7 +622,6 @@ describe('file a report on a resource', () => { id: expect.any(String), createdAt: expect.any(String), updatedAt: expect.any(String), - disable: false, closed: false, resource: { __typename: 'User', @@ -645,7 +642,6 @@ describe('file a report on a resource', () => { id: expect.any(String), createdAt: expect.any(String), updatedAt: expect.any(String), - disable: false, closed: false, resource: { __typename: 'Post', @@ -666,7 +662,6 @@ describe('file a report on a resource', () => { id: expect.any(String), createdAt: expect.any(String), updatedAt: expect.any(String), - disable: false, closed: false, resource: { __typename: 'Comment', diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index 495f150946..6da4dec9a2 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -1,5 +1,15 @@ import gql from 'graphql-tag' +export const linkableUserFragment = lang => gql` + fragment user on User { + id + slug + name + avatar + disabled + deleted + } +` export const userFragment = lang => gql` fragment user on User { id @@ -32,8 +42,6 @@ export const postCountsFragment = gql` } ` export const postFragment = lang => gql` - ${userFragment(lang)} - fragment post on Post { id title @@ -68,8 +76,6 @@ export const postFragment = lang => gql` } ` export const commentFragment = lang => gql` - ${userFragment(lang)} - fragment comment on Comment { id createdAt diff --git a/webapp/graphql/PostQuery.js b/webapp/graphql/PostQuery.js index 3de1178b0e..b6b4c2b6f2 100644 --- a/webapp/graphql/PostQuery.js +++ b/webapp/graphql/PostQuery.js @@ -1,9 +1,10 @@ import gql from 'graphql-tag' -import { postFragment, commentFragment, postCountsFragment } from './Fragments' +import { userFragment, postFragment, commentFragment, postCountsFragment } from './Fragments' export default i18n => { const lang = i18n.locale().toUpperCase() return gql` + ${userFragment(lang)} ${postFragment(lang)} ${postCountsFragment} ${commentFragment(lang)} @@ -23,6 +24,7 @@ export default i18n => { export const filterPosts = i18n => { const lang = i18n.locale().toUpperCase() return gql` + ${userFragment(lang)} ${postFragment(lang)} ${postCountsFragment} @@ -38,6 +40,7 @@ export const filterPosts = i18n => { export const profilePagePosts = i18n => { const lang = i18n.locale().toUpperCase() return gql` + ${userFragment(lang)} ${postFragment(lang)} ${postCountsFragment} @@ -66,6 +69,7 @@ export const PostsEmotionsByCurrentUser = () => { export const relatedContributions = i18n => { const lang = i18n.locale().toUpperCase() return gql` + ${userFragment(lang)} ${postFragment(lang)} ${postCountsFragment} diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 4ed2522618..b5281b6417 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -1,5 +1,5 @@ import gql from 'graphql-tag' -import { userFragment, postFragment, commentFragment } from './Fragments' +import { linkableUserFragment, userFragment, postFragment, commentFragment } from './Fragments' export default i18n => { const lang = i18n.locale().toUpperCase() @@ -49,6 +49,7 @@ export const minimisedUserQuery = () => { export const notificationQuery = i18n => { const lang = i18n.locale().toUpperCase() return gql` + ${linkableUserFragment()} ${commentFragment(lang)} ${postFragment(lang)} @@ -78,6 +79,7 @@ export const notificationQuery = i18n => { export const markAsReadMutation = i18n => { const lang = i18n.locale().toUpperCase() return gql` + ${linkableUserFragment()} ${commentFragment(lang)} ${postFragment(lang)}