diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 0ecb6c115..ff3992ead 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -57,11 +57,40 @@ const validateUpdatePost = async (resolve, root, args, context, info) => { return validatePost(resolve, root, args, context, info) } +const validateReport = async (resolve, root, args, context, info) => { + const { resourceId } = args + const { user, driver } = context + if (resourceId === user.id) throw new Error('You cannot report yourself!') + const session = driver.session() + const reportQueryRes = await session.run( + ` + MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) + RETURN labels(resource)[0] as label + `, + { + resourceId, + submitterId: user.id, + }, + ) + const [existingReportedResource] = reportQueryRes.records.map(record => { + return { + label: record.get('label'), + } + }) + + if (existingReportedResource) + throw new Error( + `You have already reported the ${existingReportedResource.label}, please only report the same ${existingReportedResource.label} once`, + ) + return resolve(root, args, context, info) +} + export default { Mutation: { CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, CreatePost: validatePost, UpdatePost: validateUpdatePost, + report: validateReport, }, } diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 3e63f1c1a..48677429a 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -1,87 +1,60 @@ export default { Mutation: { report: async (_parent, params, { driver, user }, _resolveInfo) => { + let createdRelationshipWithNestedAttributes const { resourceId, reasonCategory, reasonDescription } = params - const session = driver.session() - const reportProperties = { - createdAt: new Date().toISOString(), - reasonCategory, - reasonDescription, - } - - const reportQueryRes = await session.run( - ` - MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) - RETURN labels(resource)[0] as label - `, - { - resourceId, - submitterId: user.id, - }, - ) - const [rep] = reportQueryRes.records.map(record => { - return { - label: record.get('label'), - } + 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'), + })) }) - - if (rep) { - throw new Error(rep.label) - } - - const res = await session.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: reportProperties.createdAt, - reasonCategory: reportProperties.reasonCategory, - reasonDescription: reportProperties.reasonDescription, - }, - ) - - session.close() - - const [dbResponse] = res.records.map(r => { - return { - report: r.get('report'), - submitter: r.get('submitter'), - resource: r.get('resource'), - type: r.get('type'), + try { + const txResult = await writeTxResultPromise + if (!txResult[0]) return null + const { report, submitter, resource, type } = txResult[0] + createdRelationshipWithNestedAttributes = { + ...report.properties, + post: null, + comment: null, + user: null, + submitter: submitter.properties, + type, } - }) - if (!dbResponse) return null - - const { report, submitter, resource, type } = dbResponse - - const response = { - ...report.properties, - post: null, - comment: null, - user: null, - submitter: submitter.properties, - type, + switch (type) { + case 'Post': + createdRelationshipWithNestedAttributes.post = resource + break + case 'Comment': + createdRelationshipWithNestedAttributes.comment = resource + break + case 'User': + createdRelationshipWithNestedAttributes.user = resource + break + } + } finally { + session.close() } - switch (type) { - case 'Post': - response.post = resource.properties - break - case 'Comment': - response.comment = resource.properties - break - case 'User': - response.user = resource.properties - break - } - - return response + return createdRelationshipWithNestedAttributes }, }, Query: { @@ -113,13 +86,13 @@ export default { const responseEle = { ...report.properties, - resourceId: resource.properties.id, post: null, comment: null, user: null, submitter: submitter.properties, type, } + switch (type) { case 'Post': responseEle.post = resource.properties diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 238de25dd..4022be1b1 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -20,7 +20,7 @@ describe('report mutation', () => { const action = () => { reportMutation = gql` - mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { report( resourceId: $resourceId reasonCategory: $reasonCategory @@ -166,7 +166,7 @@ describe('report mutation', () => { reasonCategory: 'my_category', } await expect(action()).rejects.toThrow( - 'Expected a value of type "ReasonCategory" but received: "my_category"', + 'got invalid value "my_category"; Expected type ReasonCategory', ) }) @@ -307,16 +307,11 @@ describe('report mutation', () => { }) describe('reports query', () => { - let query - let mutate - let authenticatedUser = null - let moderator - let user - let author + let query, mutate, authenticatedUser, moderator, user, author const categoryIds = ['cat9'] const reportMutation = gql` - mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { report( resourceId: $resourceId reasonCategory: $reasonCategory @@ -335,7 +330,6 @@ describe('reports query', () => { submitter { id } - resourceId type user { id @@ -350,7 +344,8 @@ describe('reports query', () => { } ` - beforeAll(() => { + beforeAll(async () => { + await factory.cleanDatabase() const { server } = createServer({ context: () => { return { @@ -480,7 +475,6 @@ describe('reports query', () => { submitter: expect.objectContaining({ id: 'user1', }), - resourceId: 'auth1', type: 'User', user: expect.objectContaining({ id: 'auth1', @@ -495,7 +489,6 @@ describe('reports query', () => { submitter: expect.objectContaining({ id: 'user1', }), - resourceId: 'p1', type: 'Post', user: null, post: expect.objectContaining({ @@ -510,7 +503,6 @@ describe('reports query', () => { submitter: expect.objectContaining({ id: 'user1', }), - resourceId: 'c1', type: 'Comment', user: null, post: null, diff --git a/backend/src/schema/types/type/REPORTED.gql b/backend/src/schema/types/type/REPORTED.gql index f7be579a1..0822c14ed 100644 --- a/backend/src/schema/types/type/REPORTED.gql +++ b/backend/src/schema/types/type/REPORTED.gql @@ -5,10 +5,8 @@ type REPORTED { submitter: User @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user") # not yet supported - # resource: ReportReource + # resource: ReportResource # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") - resourceId: ID - @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource {.id}") type: String @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]") user: User @@ -29,7 +27,7 @@ enum ReasonCategory { } # not yet supported -# union ReportReource = User | Post | Comment +# union ReportResource = User | Post | Comment enum ReportOrdering { createdAt_desc @@ -40,5 +38,5 @@ type Query { } type Mutation { - report(resourceId: ID!, reasonCategory: String!, reasonDescription: String!): REPORTED + report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED } diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 8d93a90cf..76fbb4875 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -649,7 +649,7 @@ import { gql } from '../jest/helpers' // There is no error logged or the 'try' fails if this mutation is wrong. Why? const reportMutation = gql` - mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { report( resourceId: $resourceId reasonCategory: $reasonCategory diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/dbManipulations/20191011_changeReportNodeToRelationOnly.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/dbManipulations/20191011_changeReportNodeToRelationOnly.cql deleted file mode 100644 index 8c32cf5ec..000000000 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/dbManipulations/20191011_changeReportNodeToRelationOnly.cql +++ /dev/null @@ -1,7 +0,0 @@ -MATCH (submitter:User)-[:REPORTED]->(report:Report)-[:REPORTED]->(resource) -DETACH DELETE report -CREATE (submitter)-[reported:REPORTED]->(resource) -SET reported.createdAt = toString(datetime()) -SET reported.reasonCategory = 'other' -SET reported.reasonDescription = '!!! Created automatically to ensure database consistency! Date-time is this creation date and time.' -RETURN reported \ No newline at end of file diff --git a/neo4j/change_report_node_to_relationship.sh b/neo4j/change_report_node_to_relationship.sh new file mode 100755 index 000000000..a17dc6fe0 --- /dev/null +++ b/neo4j/change_report_node_to_relationship.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +ENV_FILE=$(dirname "$0")/.env +[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" + +if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then + echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." + echo "Database manipulation is not possible without connecting to the database." + echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" +fi + +until echo 'RETURN "Connection successful" as info;' | cypher-shell +do + echo "Connecting to neo4j failed, trying again..." + sleep 1 +done + +echo " +MATCH (submitter:User)-[:REPORTED]->(report:Report)-[:REPORTED]->(resource) +DETACH DELETE report +CREATE (submitter)-[reported:REPORTED]->(resource) +SET reported.createdAt = toString(datetime()) +SET reported.reasonCategory = 'other' +SET reported.reasonDescription = '!!! Created automatically to ensure database consistency! createdAt is when the database manipulation happened.' +RETURN reported; +" | cypher-shell