mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #2402 from Human-Connection/close_neo4j_driver_sessions
Close neo4j driver sessions
This commit is contained in:
commit
030fc9c5a4
@ -11,15 +11,21 @@ export default async (driver, authorizationHeader) => {
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
const session = driver.session()
|
||||
const query = `
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
SET user.lastActiveAt = toString(datetime())
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`
|
||||
const result = await session.run(query, { id })
|
||||
session.close()
|
||||
const session = driver.session()
|
||||
let result
|
||||
|
||||
try {
|
||||
result = await session.run(query, { id })
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
@ -3,7 +3,6 @@ import extractHashtags from '../hashtags/extractHashtags'
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
@ -19,14 +18,18 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||
|
||||
const postAuthorOfComment = async (comment, { context }) => {
|
||||
const session = context.driver.session()
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
@ -31,7 +35,6 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
|
||||
const session = context.driver.session()
|
||||
let cypher
|
||||
switch (reason) {
|
||||
case 'mentioned_in_post': {
|
||||
@ -85,12 +88,16 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
@ -123,15 +130,19 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
|
||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const session = context.driver.session()
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
@ -45,8 +45,8 @@ const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!user) return false
|
||||
const session = driver.session()
|
||||
const { id: resourceId } = args
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
|
||||
@ -3,11 +3,14 @@ import uniqueSlug from './slugify/uniqueSlug'
|
||||
const isUniqueFor = (context, type) => {
|
||||
return async slug => {
|
||||
const session = context.driver.session()
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
})
|
||||
session.close()
|
||||
return response.records.length === 0
|
||||
try {
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
})
|
||||
return response.records.length === 0
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||
const NO_CATEGORIES_ERR_MESSAGE =
|
||||
'You cannot save a post without at least one category or more than three'
|
||||
|
||||
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = args
|
||||
|
||||
@ -13,28 +13,30 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
}
|
||||
const session = context.driver.session()
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
try {
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const validateUpdateComment = async (resolve, root, args, context, info) => {
|
||||
const COMMENT_MIN_LENGTH = 1
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
@ -62,30 +64,33 @@ const validateReport = async (resolve, root, args, context, info) => {
|
||||
const { user, driver } = context
|
||||
if (resourceId === user.id) throw new Error('You cannot report yourself!')
|
||||
const session = driver.session()
|
||||
const reportQueryRes = await session.run(
|
||||
`
|
||||
try {
|
||||
const reportQueryRes = await session.run(
|
||||
`
|
||||
MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId})
|
||||
RETURN labels(resource)[0] as label
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
submitterId: user.id,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [existingReportedResource] = reportQueryRes.records.map(record => {
|
||||
return {
|
||||
label: record.get('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)
|
||||
if (existingReportedResource) throw new Error(`${existingReportedResource.label}`)
|
||||
return resolve(root, args, context, info)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: validateCommentCreation,
|
||||
CreateComment: validateCreateComment,
|
||||
UpdateComment: validateUpdateComment,
|
||||
CreatePost: validatePost,
|
||||
UpdatePost: validateUpdatePost,
|
||||
|
||||
244
backend/src/middleware/validation/validationMiddleware.spec.js
Normal file
244
backend/src/middleware/validation/validationMiddleware.spec.js
Normal file
@ -0,0 +1,244 @@
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../seed/factories'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../../server'
|
||||
|
||||
const factory = Factory()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
let mutate, authenticatedUser, user
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const updateCommentMutation = gql`
|
||||
mutation($content: String!, $id: ID!) {
|
||||
UpdateComment(content: $content, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await factory.create('User', {
|
||||
id: 'user-id',
|
||||
})
|
||||
await factory.create('Post', {
|
||||
id: 'post-4-commenting',
|
||||
authorId: 'user-id',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('validateCreateComment', () => {
|
||||
let createCommentVariables
|
||||
beforeEach(async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'whatever',
|
||||
content: '',
|
||||
}
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if content is empty', async () => {
|
||||
createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' }
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||
createCommentVariables = { postId: 'post-4-commenting', content: '<a></a>' }
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error if there is no post with given id in the database', async () => {
|
||||
createCommentVariables = {
|
||||
...createCommentVariables,
|
||||
postId: 'non-existent-post',
|
||||
content: 'valid content',
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment cannot be created without a post!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdateComment', () => {
|
||||
let updateCommentVariables
|
||||
beforeEach(async () => {
|
||||
await factory.create('Comment', {
|
||||
id: 'comment-id',
|
||||
authorId: 'user-id',
|
||||
})
|
||||
updateCommentVariables = {
|
||||
id: 'whatever',
|
||||
content: '',
|
||||
}
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if content is empty', async () => {
|
||||
updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' }
|
||||
await expect(
|
||||
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||
updateCommentVariables = { id: 'comment-id', content: '<a></a>' }
|
||||
await expect(
|
||||
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePost', () => {
|
||||
let createPostVariables
|
||||
beforeEach(async () => {
|
||||
createPostVariables = {
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
}
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
describe('null', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = { ...createPostVariables, categoryIds: null }
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = { ...createPostVariables, categoryIds: [] }
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('more than 3 categoryIds', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = {
|
||||
...createPostVariables,
|
||||
categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'],
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdatePost', () => {
|
||||
describe('post created without categories somehow', () => {
|
||||
let owner, updatePostVariables
|
||||
beforeEach(async () => {
|
||||
const postSomehowCreated = await neode.create('Post', {
|
||||
id: 'how-was-this-created',
|
||||
})
|
||||
owner = await neode.create('User', {
|
||||
id: 'author-of-post-without-category',
|
||||
slug: 'hacker',
|
||||
})
|
||||
await postSomehowCreated.relateTo(owner, 'author')
|
||||
authenticatedUser = await owner.toJson()
|
||||
updatePostVariables = {
|
||||
id: 'how-was-this-created',
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
categoryIds: [],
|
||||
}
|
||||
})
|
||||
|
||||
it('requires at least one category for successful update', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: updatePostMutation, variables: updatePostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdatePost: null },
|
||||
errors: [
|
||||
{ message: 'You cannot save a post without at least one category or more than three' },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -13,7 +13,8 @@ export default {
|
||||
params.id = params.id || uuid()
|
||||
|
||||
const session = context.driver.session()
|
||||
const createCommentCypher = `
|
||||
try {
|
||||
const createCommentCypher = `
|
||||
MATCH (post:Post {id: $postId})
|
||||
MATCH (author:User {id: $userId})
|
||||
WITH post, author
|
||||
@ -23,45 +24,53 @@ export default {
|
||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(createCommentCypher, {
|
||||
userId: context.user.id,
|
||||
postId,
|
||||
params,
|
||||
})
|
||||
session.close()
|
||||
const transactionRes = await session.run(createCommentCypher, {
|
||||
userId: context.user.id,
|
||||
postId,
|
||||
params,
|
||||
})
|
||||
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
|
||||
return comment
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
UpdateComment: async (_parent, params, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const updateCommentCypher = `
|
||||
try {
|
||||
const updateCommentCypher = `
|
||||
MATCH (comment:Comment {id: $params.id})
|
||||
SET comment += $params
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(updateCommentCypher, { params })
|
||||
session.close()
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
const transactionRes = await session.run(updateCommentCypher, { params })
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
DeleteComment: async (_parent, args, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $commentId})
|
||||
SET comment.deleted = TRUE
|
||||
SET comment.content = 'UNAVAILABLE'
|
||||
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||
RETURN comment
|
||||
`,
|
||||
{ commentId: args.id },
|
||||
)
|
||||
session.close()
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
{ commentId: args.id },
|
||||
)
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Comment: {
|
||||
|
||||
@ -111,42 +111,6 @@ describe('CreateComment', () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('comment content is empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: '<p></p>' }
|
||||
})
|
||||
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('comment content contains only whitespaces', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: ' <p> </p> ' }
|
||||
})
|
||||
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid post id', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, postId: 'does-not-exist' }
|
||||
})
|
||||
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -226,17 +190,6 @@ describe('UpdateComment', () => {
|
||||
expect(newlyCreatedComment.updatedAt).not.toEqual(UpdateComment.updatedAt)
|
||||
})
|
||||
|
||||
describe('if `content` empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: ' <p> </p>' }
|
||||
})
|
||||
|
||||
it('throws InputError', async () => {
|
||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('if comment does not exist for given id', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'does-not-exist' }
|
||||
|
||||
@ -2,8 +2,8 @@ export default {
|
||||
Mutation: {
|
||||
UpdateDonations: async (_parent, params, context, _resolveInfo) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
let donations
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const updateDonationsTransactionResponse = await txc.run(
|
||||
`
|
||||
|
||||
@ -4,7 +4,6 @@ export default async function createPasswordReset(options) {
|
||||
const { driver, nonce, email, issuedAt = new Date() } = options
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
let response = {}
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
||||
@ -23,9 +22,8 @@ export default async function createPasswordReset(options) {
|
||||
const { name } = record.get('u').properties
|
||||
return { email, nonce, name }
|
||||
})
|
||||
response = records[0] || {}
|
||||
return records[0] || {}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
@ -12,13 +12,16 @@ export default {
|
||||
RETURN resource {.id}
|
||||
`
|
||||
const session = driver.session()
|
||||
const res = await session.run(cypher, { id, userId })
|
||||
session.close()
|
||||
const [resource] = res.records.map(record => {
|
||||
return record.get('resource')
|
||||
})
|
||||
if (!resource) return null
|
||||
return resource.id
|
||||
try {
|
||||
const res = await session.run(cypher, { id, userId })
|
||||
const [resource] = res.records.map(record => {
|
||||
return record.get('resource')
|
||||
})
|
||||
if (!resource) return null
|
||||
return resource.id
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
enable: async (object, params, { user, driver }) => {
|
||||
const { id } = params
|
||||
@ -29,13 +32,16 @@ export default {
|
||||
RETURN resource {.id}
|
||||
`
|
||||
const session = driver.session()
|
||||
const res = await session.run(cypher, { id })
|
||||
session.close()
|
||||
const [resource] = res.records.map(record => {
|
||||
return record.get('resource')
|
||||
})
|
||||
if (!resource) return null
|
||||
return resource.id
|
||||
try {
|
||||
const res = await session.run(cypher, { id })
|
||||
const [resource] = res.records.map(record => {
|
||||
return record.get('resource')
|
||||
})
|
||||
if (!resource) return null
|
||||
return resource.id
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
notifications: async (_parent, args, context, _resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
let notifications, whereClause, orderByClause
|
||||
let whereClause, orderByClause
|
||||
|
||||
switch (args.read) {
|
||||
case true:
|
||||
@ -42,27 +42,25 @@ export default {
|
||||
}
|
||||
const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : ''
|
||||
const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : ''
|
||||
try {
|
||||
const cypher = `
|
||||
const cypher = `
|
||||
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
|
||||
${whereClause}
|
||||
RETURN resource, notification, user
|
||||
${orderByClause}
|
||||
${offset} ${limit}
|
||||
`
|
||||
try {
|
||||
const result = await session.run(cypher, { id: currentUser.id })
|
||||
notifications = await result.records.map(transformReturnType)
|
||||
return result.records.map(transformReturnType)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return notifications
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
let notification
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
@ -71,11 +69,10 @@ export default {
|
||||
`
|
||||
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||
const notifications = await result.records.map(transformReturnType)
|
||||
notification = notifications[0]
|
||||
return notifications[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return notification
|
||||
},
|
||||
},
|
||||
NOTIFIED: {
|
||||
|
||||
@ -9,7 +9,6 @@ export default {
|
||||
return createPasswordReset({ driver, nonce, email })
|
||||
},
|
||||
resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => {
|
||||
const session = driver.session()
|
||||
const stillValid = new Date()
|
||||
stillValid.setDate(stillValid.getDate() - 1)
|
||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
@ -21,16 +20,20 @@ export default {
|
||||
SET u.encryptedPassword = $encryptedNewPassword
|
||||
RETURN pr
|
||||
`
|
||||
const transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const response = !!(reset && reset.properties.usedAt)
|
||||
session.close()
|
||||
return response
|
||||
const session = driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const response = !!(reset && reset.properties.usedAt)
|
||||
return response
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -15,10 +15,13 @@ let variables
|
||||
|
||||
const getAllPasswordResets = async () => {
|
||||
const session = driver.session()
|
||||
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||
const resets = transactionRes.records.map(record => record.get('r'))
|
||||
session.close()
|
||||
return resets
|
||||
try {
|
||||
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||
const resets = transactionRes.records.map(record => record.get('r'))
|
||||
return resets
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -54,37 +54,41 @@ export default {
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { postId, data } = params
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||
`,
|
||||
{ postId, data },
|
||||
)
|
||||
session.close()
|
||||
{ postId, data },
|
||||
)
|
||||
|
||||
const [emotionsCount] = transactionRes.records.map(record => {
|
||||
return record.get('emotionsCount').low
|
||||
})
|
||||
|
||||
return emotionsCount
|
||||
const [emotionsCount] = transactionRes.records.map(record => {
|
||||
return record.get('emotionsCount').low
|
||||
})
|
||||
return emotionsCount
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { postId } = params
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
RETURN collect(emoted.emotion) as emotion`,
|
||||
{ userId: context.user.id, postId },
|
||||
)
|
||||
{ userId: context.user.id, postId },
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
const [emotions] = transactionRes.records.map(record => {
|
||||
return record.get('emotion')
|
||||
})
|
||||
return emotions
|
||||
const [emotions] = transactionRes.records.map(record => {
|
||||
return record.get('emotion')
|
||||
})
|
||||
return emotions
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
@ -93,8 +97,6 @@ export default {
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
params.id = params.id || uuid()
|
||||
let post
|
||||
|
||||
const createPostCypher = `CREATE (post:Post {params})
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
@ -113,7 +115,7 @@ export default {
|
||||
try {
|
||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||
const posts = transactionRes.records.map(record => record.get('post').properties)
|
||||
post = posts[0]
|
||||
return posts[0]
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Post with this slug already exists!')
|
||||
@ -121,55 +123,55 @@ export default {
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return post
|
||||
},
|
||||
UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
const session = context.driver.session()
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
RETURN post, category
|
||||
`
|
||||
|
||||
await session.run(cypherDeletePreviousRelations, { params })
|
||||
await session.run(cypherDeletePreviousRelations, { params })
|
||||
|
||||
updatePostCypher += `
|
||||
updatePostCypher += `
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
WITH post
|
||||
`
|
||||
}
|
||||
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
|
||||
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post').properties
|
||||
})
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
|
||||
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post').properties
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return post
|
||||
},
|
||||
|
||||
DeletePost: async (object, args, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
try {
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
@ -180,51 +182,60 @@ export default {
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
`,
|
||||
{ postId: args.id },
|
||||
)
|
||||
session.close()
|
||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||
return post
|
||||
{ postId: args.id },
|
||||
)
|
||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { to, data } = params
|
||||
const { user } = context
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
||||
RETURN userFrom, postTo, emotedRelation`,
|
||||
{ user, to, data },
|
||||
)
|
||||
session.close()
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
...record.get('emotedRelation').properties,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
{ user, to, data },
|
||||
)
|
||||
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
...record.get('emotedRelation').properties,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
RemovePostEmotions: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { to, data } = params
|
||||
const { id: from } = context.user
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
DELETE emotedRelation
|
||||
RETURN userFrom, postTo`,
|
||||
{ from, to, data },
|
||||
)
|
||||
session.close()
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
emotion: data.emotion,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
{ from, to, data },
|
||||
)
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
emotion: data.emotion,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
pinPost: async (_parent, params, context, _resolveInfo) => {
|
||||
let pinnedPostWithNestedAttributes
|
||||
@ -242,25 +253,25 @@ export default {
|
||||
)
|
||||
return deletePreviousRelationsResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
await writeTxResultPromise
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
|
||||
writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const pinPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const pinPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
|
||||
MATCH (post:Post {id: $params.id})
|
||||
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
|
||||
SET post.pinned = true
|
||||
RETURN post, pinned.createdAt as pinnedAt
|
||||
`,
|
||||
{ userId, params },
|
||||
)
|
||||
return pinPostTransactionResponse.records.map(record => ({
|
||||
pinnedPost: record.get('post').properties,
|
||||
pinnedAt: record.get('pinnedAt'),
|
||||
}))
|
||||
})
|
||||
try {
|
||||
{ userId, params },
|
||||
)
|
||||
return pinPostTransactionResponse.records.map(record => ({
|
||||
pinnedPost: record.get('post').properties,
|
||||
pinnedAt: record.get('pinnedAt'),
|
||||
}))
|
||||
})
|
||||
const [transactionResult] = await writeTxResultPromise
|
||||
const { pinnedPost, pinnedAt } = transactionResult
|
||||
pinnedPostWithNestedAttributes = {
|
||||
|
||||
@ -316,53 +316,6 @@ describe('CreatePost', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
describe('null', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: null }
|
||||
})
|
||||
it('throws UserInputError', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: [] }
|
||||
})
|
||||
it('throws UserInputError', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('more than 3 items', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
|
||||
})
|
||||
it('throws UserInputError', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -493,74 +446,6 @@ describe('UpdatePost', () => {
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
describe('more than 3 categories', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
|
||||
})
|
||||
|
||||
it('allows a maximum of three category for a successful update', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: updatePostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created without categories somehow', () => {
|
||||
let owner
|
||||
|
||||
beforeEach(async () => {
|
||||
const postSomehowCreated = await neode.create('Post', {
|
||||
id: 'how-was-this-created',
|
||||
})
|
||||
owner = await neode.create('User', {
|
||||
id: 'author-of-post-without-category',
|
||||
name: 'Hacker',
|
||||
slug: 'hacker',
|
||||
email: 'hacker@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await postSomehowCreated.relateTo(owner, 'author')
|
||||
authenticatedUser = await owner.toJson()
|
||||
variables = { ...variables, id: 'how-was-this-created' }
|
||||
})
|
||||
|
||||
it('throws an error if categoryIds is not an array', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
categoryIds: null,
|
||||
},
|
||||
})
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
|
||||
it('requires at least one category for successful update', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
categoryIds: [],
|
||||
},
|
||||
})
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -5,31 +5,31 @@ export default {
|
||||
const { resourceId, reasonCategory, reasonDescription } = params
|
||||
const { driver, user } = context
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const reportRelationshipTransactionResponse = await txc.run(
|
||||
`
|
||||
try {
|
||||
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'),
|
||||
}))
|
||||
})
|
||||
try {
|
||||
{
|
||||
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'),
|
||||
}))
|
||||
})
|
||||
const txResult = await writeTxResultPromise
|
||||
if (!txResult[0]) return null
|
||||
const { report, submitter, resource, type } = txResult[0]
|
||||
@ -61,7 +61,6 @@ export default {
|
||||
Query: {
|
||||
reports: async (_parent, params, context, _resolveInfo) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
let response
|
||||
let orderByClause
|
||||
switch (params.orderBy) {
|
||||
@ -74,6 +73,7 @@ export default {
|
||||
default:
|
||||
orderByClause = ''
|
||||
}
|
||||
const session = driver.session()
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (submitter:User)-[report:REPORTED]->(resource)
|
||||
|
||||
@ -4,48 +4,51 @@ export default {
|
||||
const { id, type } = params
|
||||
|
||||
const session = context.driver.session()
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
|
||||
WHERE $type IN labels(node) AND NOT userWritten.id = $userId
|
||||
MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node)
|
||||
RETURN COUNT(relation) > 0 as isShouted`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return isShouted
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
|
||||
unshout: async (_object, params, context, _resolveInfo) => {
|
||||
const { id, type } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||
WHERE $type IN labels(node)
|
||||
DELETE relation
|
||||
RETURN COUNT(relation) > 0 as isShouted`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
session.close()
|
||||
|
||||
return isShouted
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
Query: {
|
||||
statistics: async (parent, args, { driver, user }) => {
|
||||
statistics: async (_parent, _args, { driver }) => {
|
||||
const session = driver.session()
|
||||
const response = {}
|
||||
try {
|
||||
@ -33,10 +33,10 @@ export default {
|
||||
* Note: invites count is calculated this way because invitation codes are not in use yet
|
||||
*/
|
||||
response.countInvites = response.countEmails - response.countUsers
|
||||
return response
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return response
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
140
backend/src/schema/resolvers/statistics.spec.js
Normal file
140
backend/src/schema/resolvers/statistics.spec.js
Normal file
@ -0,0 +1,140 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory from '../../seed/factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let query, authenticatedUser
|
||||
const factory = Factory()
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
const statisticsQuery = gql`
|
||||
query {
|
||||
statistics {
|
||||
countUsers
|
||||
countPosts
|
||||
countComments
|
||||
countNotifications
|
||||
countInvites
|
||||
countFollows
|
||||
countShouts
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeAll(() => {
|
||||
authenticatedUser = undefined
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('statistics', () => {
|
||||
describe('countUsers', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
[...Array(6).keys()].map(() => {
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the count of all users', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countUsers: 6 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countPosts', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
[...Array(3).keys()].map(() => {
|
||||
return factory.create('Post')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the count of all posts', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countPosts: 3 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countComments', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('Comment')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the count of all comments', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countComments: 2 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countFollows', () => {
|
||||
let users
|
||||
beforeEach(async () => {
|
||||
users = await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
await users[0].relateTo(users[1], 'following')
|
||||
})
|
||||
|
||||
it('returns the count of all follows', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countFollows: 1 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countShouts', () => {
|
||||
let users, posts
|
||||
beforeEach(async () => {
|
||||
users = await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
posts = await Promise.all(
|
||||
[...Array(3).keys()].map(() => {
|
||||
return factory.create('Post')
|
||||
}),
|
||||
)
|
||||
await Promise.all([
|
||||
users[0].relateTo(posts[1], 'shouted'),
|
||||
users[1].relateTo(posts[0], 'shouted'),
|
||||
])
|
||||
})
|
||||
|
||||
it('returns the count of all shouts', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countShouts: 2 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -24,29 +24,32 @@ export default {
|
||||
// }
|
||||
email = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
const result = await session.run(
|
||||
`
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
||||
RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
|
||||
`,
|
||||
{ userEmail: email },
|
||||
)
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
{ userEmail: email },
|
||||
)
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
!currentUser.disabled
|
||||
) {
|
||||
delete currentUser.encryptedPassword
|
||||
return encode(currentUser)
|
||||
} else if (currentUser && currentUser.disabled) {
|
||||
throw new AuthenticationError('Your account has been disabled.')
|
||||
} else {
|
||||
throw new AuthenticationError('Incorrect email address or password.')
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
!currentUser.disabled
|
||||
) {
|
||||
delete currentUser.encryptedPassword
|
||||
return encode(currentUser)
|
||||
} else if (currentUser && currentUser.disabled) {
|
||||
throw new AuthenticationError('Your account has been disabled.')
|
||||
} else {
|
||||
throw new AuthenticationError('Incorrect email address or password.')
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
||||
|
||||
@ -27,8 +27,8 @@ const factories = {
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const session = driver.session()
|
||||
const cypher = 'MATCH (n) DETACH DELETE n'
|
||||
const session = driver.session()
|
||||
try {
|
||||
return await session.run(cypher)
|
||||
} finally {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user