diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index b094231c0..64ba60f5e 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 000000000..1e97b4d72 --- /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/reports.js b/backend/src/schema/resolvers/reports.js index a1d98bb41..7e1218ba5 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, @@ -16,8 +18,7 @@ export default { const { driver, user } = context const session = driver.session() const reportWriteTxResultPromise = session.writeTransaction(async txc => { - const reportTransactionResponse = await txc.run( - ` + const cypher = ` MATCH (submitter:User {id: $submitterId}) MATCH (resource {id: $resourceId}) WHERE resource:User OR resource:Post OR resource:Comment @@ -27,15 +28,16 @@ export default { CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) RETURN report, resource, labels(resource)[0] AS type - `, - { - resourceId, - submitterId: user.id, - createdAt: new Date().toISOString(), - reasonCategory, - reasonDescription, - }, - ) + ` + const params = { + resourceId, + submitterId: user.id, + createdAt: new Date().toISOString(), + reasonCategory, + reasonDescription, + } + const reportTransactionResponse = await txc.run(cypher, params) + log(reportTransactionResponse) return reportTransactionResponse.records.map(transformReturnType) }) try { @@ -82,24 +84,24 @@ export default { const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : '' const reportReadTxPromise = session.readTransaction(async tx => { - const allReportsTransactionResponse = await tx.run( - ` - MATCH (report:Report)-[:BELONGS_TO]->(resource) - WHERE (resource:User OR resource:Post OR resource:Comment) - ${filterClause} - WITH report, resource, - [(submitter:User)-[filed:FILED]->(report) | filed {.*, submitter: properties(submitter)} ] as filed, - [(moderator:User)-[reviewed:REVIEWED]->(report) | reviewed {.*, moderator: properties(moderator)} ] as reviewed, - [(resource)<-[:WROTE]-(author:User) | author {.*} ] as optionalAuthors, - [(resource)-[:COMMENTS]->(post:Post) | post {.*} ] as optionalCommentedPosts, - resource {.*, __typename: labels(resource)[0] } as resourceWithType - WITH report, optionalAuthors, optionalCommentedPosts, reviewed, filed, - resourceWithType {.*, post: optionalCommentedPosts[0], author: optionalAuthors[0] } as finalResource - RETURN report {.*, resource: finalResource, filed: filed, reviewed: reviewed } - ${orderByClause} - ${offset} ${limit} - `, - ) + const cypher = ` + MATCH (report:Report)-[:BELONGS_TO]->(resource) + WHERE (resource:User OR resource:Post OR resource:Comment) + ${filterClause} + WITH report, resource, + [(submitter:User)-[filed:FILED]->(report) | filed {.*, submitter: properties(submitter)} ] as filed, + [(moderator:User)-[reviewed:REVIEWED]->(report) | reviewed {.*, moderator: properties(moderator)} ] as reviewed, + [(resource)<-[:WROTE]-(author:User) | author {.*} ] as optionalAuthors, + [(resource)-[:COMMENTS]->(post:Post) | post {.*} ] as optionalCommentedPosts, + resource {.*, __typename: labels(resource)[0] } as resourceWithType + WITH report, optionalAuthors, optionalCommentedPosts, reviewed, filed, + resourceWithType {.*, post: optionalCommentedPosts[0], author: optionalAuthors[0] } as finalResource + RETURN report {.*, resource: finalResource, filed: filed, reviewed: reviewed } + ${orderByClause} + ${offset} ${limit} + ` + const allReportsTransactionResponse = await tx.run(cypher) + log(allReportsTransactionResponse) return allReportsTransactionResponse.records.map(record => record.get('report')) }) try { @@ -119,13 +121,13 @@ export default { const { id } = parent let filed const readTxPromise = session.readTransaction(async tx => { - const allReportsTransactionResponse = await tx.run( - ` + const cypher = ` MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) RETURN filed, submitter - `, - { id }, - ) + ` + const params = { id } + const allReportsTransactionResponse = await tx.run(cypher, params) + log(allReportsTransactionResponse) return allReportsTransactionResponse.records.map(record => ({ submitter: record.get('submitter').properties, filed: record.get('filed').properties, @@ -153,14 +155,14 @@ export default { const { id } = parent let reviewed const readTxPromise = session.readTransaction(async tx => { - const allReportsTransactionResponse = await tx.run( - ` - MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) - RETURN moderator, review - ORDER BY report.updatedAt DESC, review.updatedAt DESC - `, - { id }, - ) + const cypher = ` + MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) + RETURN moderator, review + ORDER BY report.updatedAt DESC, review.updatedAt DESC + ` + const params = { id } + const allReportsTransactionResponse = await tx.run(cypher, params) + log(allReportsTransactionResponse) return allReportsTransactionResponse.records.map(record => ({ review: record.get('review').properties, moderator: record.get('moderator').properties,