Update to use enum in tests, seed data, etc, refactor resolver

- Extract validations to the validations middleware to clean it up
- Remove resourceId since it throws an error in the mutation if the user
asks for it back, and the resourceId is returned in post/comment/user.id
- use writeTxResultPromise to benefit from automatic retries
- more descriptive variable naming
- extract cypher query to make db manipulation into script so that it
can be run from the command line, at least locally.
This commit is contained in:
mattwr18 2019-10-14 21:07:55 +02:00
parent cae897808b
commit faf0a15aee
7 changed files with 115 additions and 104 deletions

View File

@ -57,11 +57,40 @@ const validateUpdatePost = async (resolve, root, args, context, info) => {
return validatePost(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 { export default {
Mutation: { Mutation: {
CreateComment: validateCommentCreation, CreateComment: validateCommentCreation,
UpdateComment: validateUpdateComment, UpdateComment: validateUpdateComment,
CreatePost: validatePost, CreatePost: validatePost,
UpdatePost: validateUpdatePost, UpdatePost: validateUpdatePost,
report: validateReport,
}, },
} }

View File

@ -1,87 +1,60 @@
export default { export default {
Mutation: { Mutation: {
report: async (_parent, params, { driver, user }, _resolveInfo) => { report: async (_parent, params, { driver, user }, _resolveInfo) => {
let createdRelationshipWithNestedAttributes
const { resourceId, reasonCategory, reasonDescription } = params const { resourceId, reasonCategory, reasonDescription } = params
const session = driver.session() const session = driver.session()
const reportProperties = { const writeTxResultPromise = session.writeTransaction(async txc => {
createdAt: new Date().toISOString(), const reportRelationshipTransactionResponse = await txc.run(
reasonCategory, `
reasonDescription, MATCH (submitter:User {id: $submitterId})
} MATCH (resource {id: $resourceId})
WHERE resource:User OR resource:Comment OR resource:Post
const reportQueryRes = await session.run( CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
` RETURN report, submitter, resource, labels(resource)[0] as type
MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) `,
RETURN labels(resource)[0] as label {
`, resourceId,
{ submitterId: user.id,
resourceId, createdAt: new Date().toISOString(),
submitterId: user.id, reasonCategory,
}, reasonDescription,
) },
const [rep] = reportQueryRes.records.map(record => { )
return { return reportRelationshipTransactionResponse.records.map(record => ({
label: record.get('label'), report: record.get('report'),
} submitter: record.get('submitter'),
resource: record.get('resource').properties,
type: record.get('type'),
}))
}) })
try {
if (rep) { const txResult = await writeTxResultPromise
throw new Error(rep.label) if (!txResult[0]) return null
} const { report, submitter, resource, type } = txResult[0]
createdRelationshipWithNestedAttributes = {
const res = await session.run( ...report.properties,
` post: null,
MATCH (submitter:User {id: $submitterId}) comment: null,
MATCH (resource {id: $resourceId}) user: null,
WHERE resource:User OR resource:Comment OR resource:Post submitter: submitter.properties,
CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) type,
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'),
} }
}) switch (type) {
if (!dbResponse) return null case 'Post':
createdRelationshipWithNestedAttributes.post = resource
const { report, submitter, resource, type } = dbResponse break
case 'Comment':
const response = { createdRelationshipWithNestedAttributes.comment = resource
...report.properties, break
post: null, case 'User':
comment: null, createdRelationshipWithNestedAttributes.user = resource
user: null, break
submitter: submitter.properties, }
type, } finally {
session.close()
} }
switch (type) { return createdRelationshipWithNestedAttributes
case 'Post':
response.post = resource.properties
break
case 'Comment':
response.comment = resource.properties
break
case 'User':
response.user = resource.properties
break
}
return response
}, },
}, },
Query: { Query: {
@ -113,13 +86,13 @@ export default {
const responseEle = { const responseEle = {
...report.properties, ...report.properties,
resourceId: resource.properties.id,
post: null, post: null,
comment: null, comment: null,
user: null, user: null,
submitter: submitter.properties, submitter: submitter.properties,
type, type,
} }
switch (type) { switch (type) {
case 'Post': case 'Post':
responseEle.post = resource.properties responseEle.post = resource.properties

View File

@ -20,7 +20,7 @@ describe('report mutation', () => {
const action = () => { const action = () => {
reportMutation = gql` reportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report( report(
resourceId: $resourceId resourceId: $resourceId
reasonCategory: $reasonCategory reasonCategory: $reasonCategory
@ -166,7 +166,7 @@ describe('report mutation', () => {
reasonCategory: 'my_category', reasonCategory: 'my_category',
} }
await expect(action()).rejects.toThrow( 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', () => { describe('reports query', () => {
let query let query, mutate, authenticatedUser, moderator, user, author
let mutate
let authenticatedUser = null
let moderator
let user
let author
const categoryIds = ['cat9'] const categoryIds = ['cat9']
const reportMutation = gql` const reportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report( report(
resourceId: $resourceId resourceId: $resourceId
reasonCategory: $reasonCategory reasonCategory: $reasonCategory
@ -335,7 +330,6 @@ describe('reports query', () => {
submitter { submitter {
id id
} }
resourceId
type type
user { user {
id id
@ -350,7 +344,8 @@ describe('reports query', () => {
} }
` `
beforeAll(() => { beforeAll(async () => {
await factory.cleanDatabase()
const { server } = createServer({ const { server } = createServer({
context: () => { context: () => {
return { return {
@ -480,7 +475,6 @@ describe('reports query', () => {
submitter: expect.objectContaining({ submitter: expect.objectContaining({
id: 'user1', id: 'user1',
}), }),
resourceId: 'auth1',
type: 'User', type: 'User',
user: expect.objectContaining({ user: expect.objectContaining({
id: 'auth1', id: 'auth1',
@ -495,7 +489,6 @@ describe('reports query', () => {
submitter: expect.objectContaining({ submitter: expect.objectContaining({
id: 'user1', id: 'user1',
}), }),
resourceId: 'p1',
type: 'Post', type: 'Post',
user: null, user: null,
post: expect.objectContaining({ post: expect.objectContaining({
@ -510,7 +503,6 @@ describe('reports query', () => {
submitter: expect.objectContaining({ submitter: expect.objectContaining({
id: 'user1', id: 'user1',
}), }),
resourceId: 'c1',
type: 'Comment', type: 'Comment',
user: null, user: null,
post: null, post: null,

View File

@ -5,10 +5,8 @@ type REPORTED {
submitter: User submitter: User
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user") @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user")
# not yet supported # not yet supported
# resource: ReportReource # resource: ReportResource
# @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource")
resourceId: ID
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource {.id}")
type: String type: String
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]") @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]")
user: User user: User
@ -29,7 +27,7 @@ enum ReasonCategory {
} }
# not yet supported # not yet supported
# union ReportReource = User | Post | Comment # union ReportResource = User | Post | Comment
enum ReportOrdering { enum ReportOrdering {
createdAt_desc createdAt_desc
@ -40,5 +38,5 @@ type Query {
} }
type Mutation { type Mutation {
report(resourceId: ID!, reasonCategory: String!, reasonDescription: String!): REPORTED report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED
} }

View File

@ -649,7 +649,7 @@ import { gql } from '../jest/helpers'
// There is no error logged or the 'try' fails if this mutation is wrong. Why? // There is no error logged or the 'try' fails if this mutation is wrong. Why?
const reportMutation = gql` const reportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report( report(
resourceId: $resourceId resourceId: $resourceId
reasonCategory: $reasonCategory reasonCategory: $reasonCategory

View File

@ -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

View File

@ -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