diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 1dbd36b72..740f25ec4 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -82,6 +82,31 @@ const validateReport = async (resolve, root, args, context, info) => { return resolve(root, args, context, info) } +// const validateDecide = 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(`${existingReportedResource.label}`) +// return resolve(root, args, context, info) +// } + export default { Mutation: { CreateComment: validateCommentCreation, @@ -89,5 +114,6 @@ export default { CreatePost: validatePost, UpdatePost: validateUpdatePost, report: validateReport, + // decide: validateDecide, }, } diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 95fa9ef61..be00a1435 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -24,6 +24,7 @@ export default applyScalars( 'SocialMedia', 'NOTIFIED', 'REPORTED', + 'DECIDED', ], // add 'User' here as soon as possible }, @@ -44,6 +45,7 @@ export default applyScalars( 'EMOTED', 'NOTIFIED', 'REPORTED', + 'DECIDED', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/moderation.js b/backend/src/schema/resolvers/moderation.js index 27d8838b4..5c4087834 100644 --- a/backend/src/schema/resolvers/moderation.js +++ b/backend/src/schema/resolvers/moderation.js @@ -8,16 +8,17 @@ export default { MATCH (resource {id: $resourceId}) WHERE resource:User OR resource:Comment OR resource:Post SET resource.disabled = true - MERGE (resource)<-[decided:DECIDED]-(u) + MERGE (resource)<-[decision:DECIDED]-(u) SET ( CASE - WHEN decided.createdAt IS NOT NULL - THEN decided END).updatedAt = toString(datetime()) + WHEN decision.createdAt IS NOT NULL + THEN decision END).updatedAt = toString(datetime()) SET ( CASE - WHEN decided.createdAt IS NULL - THEN decided END).createdAt = toString(datetime()) - SET decided.disabled = true + WHEN decision.createdAt IS NULL + THEN decision END).createdAt = toString(datetime()) + SET decision.disabled = true + SET decision.closed = false RETURN resource {.id} ` const session = driver.session() @@ -32,14 +33,14 @@ export default { enable: async (object, params, { user, driver }) => { const { id: resourceId } = params const cypher = ` - MATCH (resource {id: $resourceId})<-[decided:DECIDED]-(:User) + MATCH (resource {id: $resourceId})<-[decision:DECIDED]-(:User) SET resource.disabled = false - DELETE decided + DELETE decision RETURN resource {.id} ` // Wolle - // SET decided.updatedAt = toString(datetime()) - // SET decided.disabled = false + // SET decision.updatedAt = toString(datetime()) + // SET decision.disabled = false const session = driver.session() const res = await session.run(cypher, { resourceId }) session.close() @@ -49,50 +50,125 @@ export default { if (!resource) return null return resource.id }, - decide: async (object, params, { user, driver }) => { - const { resourceId, disabled } = params - const { id: userId } = user - // is there an open decision then set params - const cypher = ` - MATCH (u:User {id: $userId})-[decision:DECIDED {closed: false}]->(resource {id: $resourceId}) - WHERE NOT decision AND (resource: User OR resource: Comment OR resource: Post) - RETURN decision - ` - const session = driver.session() - const res = await session.run(cypher, { resourceId, userId }) - session.close() - const [decision] = res.records.map(record => { - return record.get('decision') - }) - if (!resource) return null - // is there no open decision then create one + decide: async (object, params, context, _resolveInfo) => { + let createdRelationshipWithNestedAttributes = null + const { resourceId, disabled, closed } = params + const { user: moderator, driver } = context - - const cypher = ` - MATCH (u:User {id: $userId}) - MATCH (resource {id: $resourceId}) - WHERE resource:User OR resource:Comment OR resource:Post - SET resource.disabled = true - MERGE (resource)<-[decided:DECIDED]-(u) - SET ( - CASE - WHEN decided.createdAt IS NOT NULL - THEN decided END).updatedAt = toString(datetime()) - SET ( - CASE - WHEN decided.createdAt IS NULL - THEN decided END).createdAt = toString(datetime()) - SET decided.disabled = true - RETURN resource {.id} - ` const session = driver.session() - const res = await session.run(cypher, { resourceId, userId }) - session.close() - const [resource] = res.records.map(record => { - return record.get('resource') + + const existingDecisionWriteTxResultPromise = session.writeTransaction(async txc => { + const decisionRelationshipTransactionResponse = await txc.run( + ` + MATCH (moderator:User)-[decision:DECIDED {closed: false}]->(resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + RETURN decision, moderator {.id} AS decisionModerator + `, { resourceId }, + ) + return decisionRelationshipTransactionResponse.records.map(record => ({ + decision: record.get('decision'), + decisionModerator: record.get('decisionModerator'), + })) }) - if (!resource) return null - return resource.id + + try { + const cypherHeader = '' + + // is there an open decision? + const existingDecisionTxResult = await existingDecisionWriteTxResultPromise + if (!existingDecisionTxResult[0]) { + // no open decision, then create one + if (!disabled) disabled = false // default for creation + if (!disabled) closed = false // default for creation + cypherHeader = ` + MATCH (moderator:User {id: $moderatorId}) + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + CREATE (resource)<-[decision:DECIDED]-(moderator) + ` + } else { + // an open decision … + + if (!disabled) disabled = existingDecisionTxResult[0].decision.properties.disabled // default set to existing + if (!disabled) closed = existingDecisionTxResult[0].decision.properties.closed // default set to existing + // current moderator is not the same as old + if (moderator.id !== existingDecisionTxResult[0].decisionModerator.id) { + // an open decision from different moderator, then change relation and properties + cypherHeader = ` + MATCH (moderator:User)-[oldDecision:DECIDED {closed: false}]->(resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + DELETE oldDecision + MATCH (moderator:User {id: $moderatorId}) + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + CREATE (resource)<-[decision:DECIDED]-(moderator) + SET decision = oldDecision + ` + } else { + // an open decision from same moderator, then change properties + cypherHeader = ` + MATCH (moderator:User)-[decision:DECIDED {closed: false}]->(resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + ` + } + } + + const newDecisionWriteTxResultPromise = session.writeTransaction(async txc => { + const decisionRelationshipTransactionResponse = await txc.run( + cypherHeader + ` + SET ( + CASE + WHEN decision.createdAt IS NOT NULL + THEN decision END).updatedAt = toString(datetime()) + SET ( + CASE + WHEN decision.createdAt IS NULL + THEN decision END).createdAt = toString(datetime()) + SET decision.disabled = $disabled + SET decision.closed = $closed + SET resource.disabled = $disabled + RETURN decision, resource, moderator, labels(resource)[0] AS type + `, { + resourceId, + moderatorId: moderator.id, + disabled, + closed, + }, + ) + return decisionRelationshipTransactionResponse.records.map(record => ({ + decision: record.get('decision'), + resource: record.get('resource'), + moderator: record.get('moderator'), + type: record.get('type'), + })) + }) + const txResult = await newDecisionWriteTxResultPromise + if (!txResult[0]) return null + const { decision, resource, moderator: moderatorInResult, type } = txResult[0] + createdRelationshipWithNestedAttributes = { + ...decision.properties, + moderator: moderatorInResult.properties, + type, + post: null, + comment: null, + user: null, + } + switch (type) { + case 'Post': + createdRelationshipWithNestedAttributes.post = resource.properties + break + case 'Comment': + createdRelationshipWithNestedAttributes.comment = resource.properties + break + case 'User': + createdRelationshipWithNestedAttributes.user = resource.properties + break + } + } finally { + session.close() + } + + return createdRelationshipWithNestedAttributes }, }, } diff --git a/backend/src/schema/types/type/DECIDED.gql b/backend/src/schema/types/type/DECIDED.gql new file mode 100644 index 000000000..0097cecfc --- /dev/null +++ b/backend/src/schema/types/type/DECIDED.gql @@ -0,0 +1,23 @@ +type DECIDED { + createdAt: String! + updatedAt: String + # reasonCategory: ReasonCategory + # reasonDescription: String + disabled: Boolean! + closed: Boolean! + + moderator: User + @cypher(statement: "MATCH (resource)<-[:DECIDED]-(moderator:User) RETURN moderator") + # not yet supported + # resource: ReportResource + # @cypher(statement: "MATCH (resource)<-[:DECIDED]-(:User) RETURN resource") + type: String + @cypher(statement: "MATCH (resource)<-[:DECIDED]-(:User) RETURN labels(resource)[0]") + user: User + post: Post + comment: Comment +} + +type Mutation { + decide(resourceId: ID!, disabled: Boolean, closed: Boolean): DECIDED +} diff --git a/neo4j/change_disabled_relationship_to_decided_relationship.sh b/neo4j/change_disabled_relationship_to_decided_relationship.sh index 76703b373..503af2bd5 100755 --- a/neo4j/change_disabled_relationship_to_decided_relationship.sh +++ b/neo4j/change_disabled_relationship_to_decided_relationship.sh @@ -18,8 +18,9 @@ done echo " MATCH (moderator:User)-[disabled:DISABLED]->(resource) DELETE disabled -CREATE (moderator)-[decided:DECIDED]->(resource) -SET decided.createdAt = toString(datetime()) -SET decided.disabled = true -RETURN decided; +CREATE (moderator)-[decision:DECIDED]->(resource) +SET decision.createdAt = toString(datetime()) +SET decision.disabled = true +SET decision.closed = false +RETURN decision; " | cypher-shell