mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #2433 from Human-Connection/2412-favor-transaction-function
Favor transaction functions
This commit is contained in:
commit
56c5f4a384
@ -11,27 +11,28 @@ export default async (driver, authorizationHeader) => {
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
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 session = driver.session()
|
||||
let result
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateUserLastActiveTransactionResponse = await transaction.run(
|
||||
`
|
||||
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
|
||||
`,
|
||||
{ id },
|
||||
)
|
||||
return updateUserLastActiveTransactionResponse.records.map(record => record.get('user'))
|
||||
})
|
||||
try {
|
||||
result = await session.run(query, { id })
|
||||
const [currentUser] = await writeTxResultPromise
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,30 +2,23 @@ import extractHashtags from '../hashtags/extractHashtags'
|
||||
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
// 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.
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p: Post { id: $postId})
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (t: Tag { id: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
const session = context.driver.session()
|
||||
|
||||
try {
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
await session.writeTransaction(txc => {
|
||||
return txc.run(
|
||||
`
|
||||
MATCH (post:Post { id: $postId})
|
||||
OPTIONAL MATCH (post)-[previousRelations:TAGGED]->(tag:Tag)
|
||||
DELETE previousRelations
|
||||
WITH post
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (tag:Tag {id: tagName, disabled: false, deleted: false })
|
||||
MERGE (post)-[:TAGGED]->(tag)
|
||||
RETURN post, tag
|
||||
`,
|
||||
{ postId, hashtags },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -7,7 +7,7 @@ import sluggify from './sluggifyMiddleware'
|
||||
import excerpt from './excerptMiddleware'
|
||||
import xss from './xssMiddleware'
|
||||
import permissions from './permissionsMiddleware'
|
||||
import user from './userMiddleware'
|
||||
import user from './user/userMiddleware'
|
||||
import includedFields from './includedFieldsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
import validation from './validation/validationMiddleware'
|
||||
|
||||
@ -38,7 +38,7 @@ const createLocation = async (session, mapboxData) => {
|
||||
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
|
||||
}
|
||||
|
||||
let query =
|
||||
let mutation =
|
||||
'MERGE (l:Location {id: $id}) ' +
|
||||
'SET l.name = $nameEN, ' +
|
||||
'l.nameEN = $nameEN, ' +
|
||||
@ -53,19 +53,23 @@ const createLocation = async (session, mapboxData) => {
|
||||
'l.type = $type'
|
||||
|
||||
if (data.lat && data.lng) {
|
||||
query += ', l.lat = $lat, l.lng = $lng'
|
||||
mutation += ', l.lat = $lat, l.lng = $lng'
|
||||
}
|
||||
query += ' RETURN l.id'
|
||||
mutation += ' RETURN l.id'
|
||||
|
||||
await session.run(query, data)
|
||||
session.close()
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(mutation, data)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const createOrUpdateLocations = async (userId, locationName, driver) => {
|
||||
if (isEmpty(locationName)) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
|
||||
locationName,
|
||||
@ -106,33 +110,44 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
|
||||
if (data.context) {
|
||||
await asyncForEach(data.context, async ctx => {
|
||||
await createLocation(session, ctx)
|
||||
|
||||
await session.run(
|
||||
'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' +
|
||||
'MERGE (child)<-[:IS_IN]-(parent) ' +
|
||||
'RETURN child.id, parent.id',
|
||||
{
|
||||
parentId: parent.id,
|
||||
childId: ctx.id,
|
||||
},
|
||||
)
|
||||
|
||||
parent = ctx
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
|
||||
MERGE (child)<-[:IS_IN]-(parent)
|
||||
RETURN child.id, parent.id
|
||||
`,
|
||||
{
|
||||
parentId: parent.id,
|
||||
childId: ctx.id,
|
||||
},
|
||||
)
|
||||
})
|
||||
parent = ctx
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
// delete all current locations from user
|
||||
await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', {
|
||||
userId: userId,
|
||||
})
|
||||
// connect user with location
|
||||
await session.run(
|
||||
'MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id',
|
||||
{
|
||||
userId: userId,
|
||||
locationId: data.id,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
// delete all current locations from user and add new location
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location)
|
||||
DETACH DELETE relationship
|
||||
WITH user
|
||||
MATCH (location:Location {id: $locationId})
|
||||
MERGE (user)-[:IS_IN]->(location)
|
||||
RETURN location.id, user.id
|
||||
`,
|
||||
{ userId: userId, locationId: data.id },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default createOrUpdateLocations
|
||||
|
||||
@ -1,164 +1,121 @@
|
||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||
import { validateNotifyUsers } from '../validation/validationMiddleware'
|
||||
|
||||
const postAuthorOfComment = async (comment, { context }) => {
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
if (post && idsOfUsers && idsOfUsers.length)
|
||||
await notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { content } = args
|
||||
let idsOfUsers = extractMentionedUsers(content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
|
||||
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
|
||||
if (idsOfUsers && idsOfUsers.length)
|
||||
await notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
if (context.user.id !== postAuthor.id)
|
||||
await notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context)
|
||||
return comment
|
||||
}
|
||||
|
||||
const postAuthorOfComment = async (commentId, { context }) => {
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
let postAuthorId
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
postAuthorId = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (author:User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN author { .id } as authorId
|
||||
`,
|
||||
{ commentId },
|
||||
)
|
||||
})
|
||||
return postAuthorId.records.map(record => record.get('authorId'))
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
return postAuthor
|
||||
}
|
||||
|
||||
const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
if (!idsOfUsers.length) return
|
||||
|
||||
// Checked here, because it does not go through GraphQL checks at all in this file.
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) {
|
||||
throw new Error('Notification reason is not allowed!')
|
||||
}
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
|
||||
let cypher
|
||||
const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
||||
await validateNotifyUsers(label, reason)
|
||||
let mentionedCypher
|
||||
switch (reason) {
|
||||
case 'mentioned_in_post': {
|
||||
cypher = `
|
||||
mentionedCypher = `
|
||||
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'mentioned_in_comment': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'commented_on_post': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (author)<-[:BLOCKED]-(user)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
mentionedCypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
`
|
||||
break
|
||||
}
|
||||
}
|
||||
mentionedCypher += `
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(mentionedCypher, { id, idsOfUsers, reason })
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
|
||||
await validateNotifyUsers(label, reason)
|
||||
const session = context.driver.session()
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (post) {
|
||||
await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
let idsOfUsers = extractMentionedUsers(args.content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const postAuthor = await postAuthorOfComment(comment, { context })
|
||||
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
|
||||
|
||||
await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
const handleCreateComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
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')
|
||||
try {
|
||||
await session.writeTransaction(async transaction => {
|
||||
await transaction.run(
|
||||
`
|
||||
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
|
||||
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`,
|
||||
{ commentId, postAuthorId, reason },
|
||||
)
|
||||
})
|
||||
if (context.user.id !== postAuthor.id) {
|
||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
CreateComment: handleCreateComment,
|
||||
CreateComment: handleContentDataOfComment,
|
||||
UpdateComment: handleContentDataOfComment,
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,11 +4,7 @@ import { createTestClient } from 'apollo-server-testing'
|
||||
import { getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
let query
|
||||
let mutate
|
||||
let notifiedUser
|
||||
let authenticatedUser
|
||||
let server, query, mutate, notifiedUser, authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
@ -39,7 +35,8 @@ const createCommentMutation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
@ -173,7 +170,6 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -190,7 +186,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -214,7 +210,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -265,7 +261,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -409,7 +405,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -467,7 +463,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -501,7 +497,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -532,7 +528,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
|
||||
@ -47,17 +47,18 @@ const isAuthor = rule({
|
||||
if (!user) return false
|
||||
const { id: resourceId } = args
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
const authorReadTxPromise = session.readTransaction(async transaction => {
|
||||
const authorTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
|
||||
RETURN author
|
||||
`,
|
||||
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
|
||||
RETURN author
|
||||
`,
|
||||
{ resourceId, userId: user.id },
|
||||
)
|
||||
const [author] = result.records.map(record => {
|
||||
return record.get('author')
|
||||
})
|
||||
return authorTransactionResponse.records.map(record => record.get('author'))
|
||||
})
|
||||
try {
|
||||
const [author] = await authorReadTxPromise
|
||||
return !!author
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -4,10 +4,16 @@ const isUniqueFor = (context, type) => {
|
||||
return async slug => {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
const existingSlug = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH(p:${type} {slug: $slug })
|
||||
RETURN p.slug
|
||||
`,
|
||||
{ slug },
|
||||
)
|
||||
})
|
||||
return response.records.length === 0
|
||||
return existingSlug.records.length === 0
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import createOrUpdateLocations from './nodes/locations'
|
||||
import createOrUpdateLocations from '../nodes/locations'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const result = await resolve(root, args, context, info)
|
||||
await createOrUpdateLocations(args.id, args.locationName, context.driver)
|
||||
await createOrUpdateLocations(result.id, args.locationName, context.driver)
|
||||
return result
|
||||
},
|
||||
UpdateUser: async (resolve, root, args, context, info) => {
|
||||
213
backend/src/middleware/user/userMiddleware.spec.js
Normal file
213
backend/src/middleware/user/userMiddleware.spec.js
Normal file
@ -0,0 +1,213 @@
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../seed/factories'
|
||||
import { 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 authenticatedUser, mutate, variables
|
||||
|
||||
const signupVerificationMutation = gql`
|
||||
mutation(
|
||||
$name: String!
|
||||
$password: String!
|
||||
$email: String!
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
$locationName: String
|
||||
) {
|
||||
SignupVerification(
|
||||
name: $name
|
||||
password: $password
|
||||
email: $email
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locationName: $locationName
|
||||
) {
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String!, $locationName: String) {
|
||||
UpdateUser(id: $id, name: $name, locationName: $locationName) {
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
lng: 41.1534,
|
||||
nameES: 'Hamburg',
|
||||
nameFR: 'Hamburg',
|
||||
nameIT: 'Hamburg',
|
||||
nameEN: 'Hamburg',
|
||||
type: 'place',
|
||||
namePT: 'Hamburg',
|
||||
nameRU: 'Хамбург',
|
||||
nameDE: 'Hamburg',
|
||||
nameNL: 'Hamburg',
|
||||
name: 'Hamburg',
|
||||
namePL: 'Hamburg',
|
||||
id: 'place.5977106083398860',
|
||||
lat: -74.5763,
|
||||
},
|
||||
state: {
|
||||
namePT: 'Nova Jérsia',
|
||||
nameRU: 'Нью-Джерси',
|
||||
nameDE: 'New Jersey',
|
||||
nameNL: 'New Jersey',
|
||||
nameES: 'Nueva Jersey',
|
||||
name: 'New Jersey',
|
||||
namePL: 'New Jersey',
|
||||
nameFR: 'New Jersey',
|
||||
nameIT: 'New Jersey',
|
||||
id: 'region.14919479731700330',
|
||||
nameEN: 'New Jersey',
|
||||
type: 'region',
|
||||
},
|
||||
country: {
|
||||
namePT: 'Estados Unidos',
|
||||
nameRU: 'Соединённые Штаты Америки',
|
||||
nameDE: 'Vereinigte Staaten',
|
||||
nameNL: 'Verenigde Staten van Amerika',
|
||||
nameES: 'Estados Unidos',
|
||||
namePL: 'Stany Zjednoczone',
|
||||
name: 'United States of America',
|
||||
nameFR: 'États-Unis',
|
||||
nameIT: "Stati Uniti d'America",
|
||||
id: 'country.9053006287256050',
|
||||
nameEN: 'United States of America',
|
||||
type: 'country',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
variables = {}
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('userMiddleware', () => {
|
||||
describe('SignupVerification', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'John Doe',
|
||||
password: '123',
|
||||
email: 'john@example.org',
|
||||
nonce: '123456',
|
||||
termsAndConditionsAgreedVersion: '0.1.0',
|
||||
locationName: 'Hamburg, New Jersey, United States of America',
|
||||
}
|
||||
const args = {
|
||||
email: 'john@example.org',
|
||||
nonce: '123456',
|
||||
}
|
||||
await neode.model('EmailAddress').create(args)
|
||||
})
|
||||
it('creates a Location node with localised city/state/country names', async () => {
|
||||
await mutate({ mutation: signupVerificationMutation, variables })
|
||||
const locations = await neode.cypher(
|
||||
`MATCH (city:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city, state, country`,
|
||||
)
|
||||
expect(
|
||||
locations.records.map(record => {
|
||||
return {
|
||||
city: record.get('city').properties,
|
||||
state: record.get('state').properties,
|
||||
country: record.get('country').properties,
|
||||
}
|
||||
}),
|
||||
).toEqual(newlyCreatedNodesWithLocales)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
let user, userParams
|
||||
beforeEach(async () => {
|
||||
newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
lng: 53.55,
|
||||
nameES: 'Hamburgo',
|
||||
nameFR: 'Hambourg',
|
||||
nameIT: 'Amburgo',
|
||||
nameEN: 'Hamburg',
|
||||
type: 'region',
|
||||
namePT: 'Hamburgo',
|
||||
nameRU: 'Гамбург',
|
||||
nameDE: 'Hamburg',
|
||||
nameNL: 'Hamburg',
|
||||
namePL: 'Hamburg',
|
||||
name: 'Hamburg',
|
||||
id: 'region.10793468240398860',
|
||||
lat: 10,
|
||||
},
|
||||
country: {
|
||||
namePT: 'Alemanha',
|
||||
nameRU: 'Германия',
|
||||
nameDE: 'Deutschland',
|
||||
nameNL: 'Duitsland',
|
||||
nameES: 'Alemania',
|
||||
name: 'Germany',
|
||||
namePL: 'Niemcy',
|
||||
nameFR: 'Allemagne',
|
||||
nameIT: 'Germania',
|
||||
id: 'country.10743216036480410',
|
||||
nameEN: 'Germany',
|
||||
type: 'country',
|
||||
},
|
||||
},
|
||||
]
|
||||
userParams = {
|
||||
id: 'updating-user',
|
||||
}
|
||||
user = await factory.create('User', userParams)
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('creates a Location node with localised city/state/country names', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'updating-user',
|
||||
name: 'Updating user',
|
||||
locationName: 'Hamburg, Germany',
|
||||
}
|
||||
await mutate({ mutation: updateUserMutation, variables })
|
||||
const locations = await neode.cypher(
|
||||
`MATCH (city:Location)-[:IS_IN]->(country:Location) return city, country`,
|
||||
)
|
||||
expect(
|
||||
locations.records.map(record => {
|
||||
return {
|
||||
city: record.get('city').properties,
|
||||
country: record.get('country').properties,
|
||||
}
|
||||
}),
|
||||
).toEqual(newlyCreatedNodesWithLocales)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -4,7 +4,7 @@ const COMMENT_MIN_LENGTH = 1
|
||||
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 USERNAME_MIN_LENGTH = 3
|
||||
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = args
|
||||
@ -14,14 +14,15 @@ const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
}
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
const postQueryRes = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post
|
||||
`,
|
||||
{ postId },
|
||||
)
|
||||
})
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
@ -72,8 +73,8 @@ const validateReview = async (resolve, root, args, context, info) => {
|
||||
const { user, driver } = context
|
||||
if (resourceId === user.id) throw new Error('You cannot review yourself!')
|
||||
const session = driver.session()
|
||||
const reportReadTxPromise = session.writeTransaction(async txc => {
|
||||
const validateReviewTransactionResponse = await txc.run(
|
||||
const reportReadTxPromise = session.readTransaction(async transaction => {
|
||||
const validateReviewTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})
|
||||
WHERE resource:User OR resource:Post OR resource:Comment
|
||||
@ -115,12 +116,31 @@ const validateReview = async (resolve, root, args, context, info) => {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
export const validateNotifyUsers = async (label, reason) => {
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) throw new Error('Notification reason is not allowed!')
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
}
|
||||
|
||||
const validateUpdateUser = async (resolve, root, params, context, info) => {
|
||||
const { name } = params
|
||||
if (typeof name === 'string' && name.trim().length < USERNAME_MIN_LENGTH)
|
||||
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} character long!`)
|
||||
return resolve(root, params, context, info)
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: validateCreateComment,
|
||||
UpdateComment: validateUpdateComment,
|
||||
CreatePost: validatePost,
|
||||
UpdatePost: validateUpdatePost,
|
||||
UpdateUser: validateUpdateUser,
|
||||
fileReport: validateReport,
|
||||
review: validateReview,
|
||||
},
|
||||
|
||||
@ -71,6 +71,14 @@ const reviewMutation = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
@ -397,4 +405,33 @@ describe('validateReview', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdateUser', () => {
|
||||
let userParams, variables, updatingUser
|
||||
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
id: 'updating-user',
|
||||
name: 'John Doe',
|
||||
}
|
||||
|
||||
variables = {
|
||||
id: 'updating-user',
|
||||
name: 'John Doughnut',
|
||||
}
|
||||
updatingUser = await factory.create('User', userParams)
|
||||
authenticatedUser = await updatingUser.toJson()
|
||||
})
|
||||
|
||||
it('with name too short', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: ' ',
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: null },
|
||||
errors: [{ message: 'Username must be at least 3 character long!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,6 +5,7 @@ export default {
|
||||
Mutation: {
|
||||
CreateComment: async (object, params, context, resolveInfo) => {
|
||||
const { postId } = params
|
||||
const { user, driver } = context
|
||||
// Adding relationship from comment to post by passing in the postId,
|
||||
// but we do not want to create the comment with postId as an attribute
|
||||
// because we use relationships for this. So, we are deleting it from params
|
||||
@ -12,26 +13,28 @@ export default {
|
||||
delete params.postId
|
||||
params.id = params.id || uuid()
|
||||
|
||||
const session = context.driver.session()
|
||||
const session = driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const createCommentTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
MATCH (author:User {id: $userId})
|
||||
WITH post, author
|
||||
CREATE (comment:Comment {params})
|
||||
SET comment.createdAt = toString(datetime())
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||
RETURN comment
|
||||
`,
|
||||
{ userId: user.id, postId, params },
|
||||
)
|
||||
return createCommentTransactionResponse.records.map(
|
||||
record => record.get('comment').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const createCommentCypher = `
|
||||
MATCH (post:Post {id: $postId})
|
||||
MATCH (author:User {id: $userId})
|
||||
WITH post, author
|
||||
CREATE (comment:Comment {params})
|
||||
SET comment.createdAt = toString(datetime())
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(createCommentCypher, {
|
||||
userId: context.user.id,
|
||||
postId,
|
||||
params,
|
||||
})
|
||||
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
|
||||
const [comment] = await writeTxResultPromise
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
@ -39,15 +42,22 @@ export default {
|
||||
},
|
||||
UpdateComment: async (_parent, params, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateCommentTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $params.id})
|
||||
SET comment += $params
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
RETURN comment
|
||||
`,
|
||||
{ params },
|
||||
)
|
||||
return updateCommentTransactionResponse.records.map(
|
||||
record => record.get('comment').properties,
|
||||
)
|
||||
})
|
||||
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 })
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
const [comment] = await writeTxResultPromise
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
@ -55,18 +65,23 @@ export default {
|
||||
},
|
||||
DeleteComment: async (_parent, args, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
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
|
||||
`,
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deleteCommentTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $commentId})
|
||||
SET comment.deleted = TRUE
|
||||
SET comment.content = 'UNAVAILABLE'
|
||||
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||
RETURN comment
|
||||
`,
|
||||
{ commentId: args.id },
|
||||
)
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return deleteCommentTransactionResponse.records.map(
|
||||
record => record.get('comment').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const [comment] = await writeTxResultPromise
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -10,7 +10,8 @@ const factory = Factory()
|
||||
|
||||
let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
@ -19,8 +20,7 @@ beforeAll(() => {
|
||||
}
|
||||
},
|
||||
})
|
||||
const client = createTestClient(server)
|
||||
mutate = client.mutate
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -100,6 +100,7 @@ describe('CreateComment', () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { content: "I'm authorised to comment" } },
|
||||
errors: undefined,
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -108,6 +109,7 @@ describe('CreateComment', () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { author: { name: 'Author' } } },
|
||||
errors: undefined,
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -157,6 +159,7 @@ describe('UpdateComment', () => {
|
||||
it('updates the comment', async () => {
|
||||
const expected = {
|
||||
data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -172,6 +175,7 @@ describe('UpdateComment', () => {
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
|
||||
@ -5,24 +5,29 @@ export default async function createPasswordReset(options) {
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
||||
CREATE(pr:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||
MERGE (u)-[:REQUESTED]->(pr)
|
||||
RETURN e, pr, u
|
||||
`
|
||||
const transactionRes = await session.run(cypher, {
|
||||
issuedAt: issuedAt.toISOString(),
|
||||
nonce,
|
||||
email: normalizedEmail,
|
||||
const createPasswordResetTxPromise = session.writeTransaction(async transaction => {
|
||||
const createPasswordResetTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email:$email})
|
||||
CREATE(passwordReset:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||
MERGE (user)-[:REQUESTED]->(passwordReset)
|
||||
RETURN email, passwordReset, user
|
||||
`,
|
||||
{
|
||||
issuedAt: issuedAt.toISOString(),
|
||||
nonce,
|
||||
email: normalizedEmail,
|
||||
},
|
||||
)
|
||||
return createPasswordResetTransactionResponse.records.map(record => {
|
||||
const { email } = record.get('email').properties
|
||||
const { nonce } = record.get('passwordReset').properties
|
||||
const { name } = record.get('user').properties
|
||||
return { email, nonce, name }
|
||||
})
|
||||
})
|
||||
const records = transactionRes.records.map(record => {
|
||||
const { email } = record.get('e').properties
|
||||
const { nonce } = record.get('pr').properties
|
||||
const { name } = record.get('u').properties
|
||||
return { email, nonce, name }
|
||||
})
|
||||
return records[0] || {}
|
||||
const [records] = await createPasswordResetTxPromise
|
||||
return records || {}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import createPasswordReset from './createPasswordReset'
|
||||
|
||||
describe('createPasswordReset', () => {
|
||||
const issuedAt = new Date()
|
||||
const nonce = 'abcdef'
|
||||
|
||||
describe('email lookup', () => {
|
||||
let driver
|
||||
let mockSession
|
||||
beforeEach(() => {
|
||||
mockSession = {
|
||||
close() {},
|
||||
run: jest.fn().mockReturnValue({
|
||||
records: {
|
||||
map: jest.fn(() => []),
|
||||
},
|
||||
}),
|
||||
}
|
||||
driver = { session: () => mockSession }
|
||||
})
|
||||
|
||||
it('lowercases email address', async () => {
|
||||
const email = 'stRaNGeCaSiNG@ExAmplE.ORG'
|
||||
await createPasswordReset({ driver, email, issuedAt, nonce })
|
||||
expect(mockSession.run.mock.calls).toEqual([
|
||||
[
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
email: 'strangecasing@example.org',
|
||||
}),
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,25 +1,29 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
export default async function alreadyExistingMail({ args, context }) {
|
||||
const cypher = `
|
||||
MATCH (email:EmailAddress {email: $email})
|
||||
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
|
||||
RETURN email, user
|
||||
`
|
||||
let transactionRes
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
transactionRes = await session.run(cypher, { email: args.email })
|
||||
const existingEmailAddressTxPromise = session.writeTransaction(async transaction => {
|
||||
const existingEmailAddressTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (email:EmailAddress {email: $email})
|
||||
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
|
||||
RETURN email, user
|
||||
`,
|
||||
{ email: args.email },
|
||||
)
|
||||
return existingEmailAddressTransactionResponse.records.map(record => {
|
||||
return {
|
||||
alreadyExistingEmail: record.get('email').properties,
|
||||
user: record.get('user') && record.get('user').properties,
|
||||
}
|
||||
})
|
||||
})
|
||||
const [emailBelongsToUser] = await existingEmailAddressTxPromise
|
||||
const { alreadyExistingEmail, user } = emailBelongsToUser || {}
|
||||
if (user) throw new UserInputError('A user account with this email already exists.')
|
||||
return alreadyExistingEmail
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [result] = transactionRes.records.map(record => {
|
||||
return {
|
||||
alreadyExistingEmail: record.get('email').properties,
|
||||
user: record.get('user') && record.get('user').properties,
|
||||
}
|
||||
})
|
||||
const { alreadyExistingEmail, user } = result || {}
|
||||
if (user) throw new UserInputError('A user account with this email already exists.')
|
||||
return alreadyExistingEmail
|
||||
}
|
||||
|
||||
@ -76,16 +76,21 @@ export default {
|
||||
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const markNotificationAsReadTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
SET notification.read = TRUE
|
||||
RETURN resource, notification, user
|
||||
`,
|
||||
{ resourceId: args.id, id: currentUser.id },
|
||||
)
|
||||
log(markNotificationAsReadTransactionResponse)
|
||||
return markNotificationAsReadTransactionResponse.records.map(transformReturnType)
|
||||
})
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
SET notification.read = TRUE
|
||||
RETURN resource, notification, user
|
||||
`
|
||||
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||
log(result)
|
||||
const notifications = await result.records.map(transformReturnType)
|
||||
return notifications[0]
|
||||
const [notifications] = await writeTxResultPromise
|
||||
return notifications
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -12,25 +12,29 @@ export default {
|
||||
const stillValid = new Date()
|
||||
stillValid.setDate(stillValid.getDate() - 1)
|
||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
const cypher = `
|
||||
MATCH (pr:PasswordReset {nonce: $nonce})
|
||||
MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr)
|
||||
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
||||
SET pr.usedAt = datetime()
|
||||
SET u.encryptedPassword = $encryptedNewPassword
|
||||
RETURN pr
|
||||
`
|
||||
const session = driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
const passwordResetTxPromise = session.writeTransaction(async transaction => {
|
||||
const passwordResetTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (passwordReset:PasswordReset {nonce: $nonce})
|
||||
MATCH (email:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(user:User)-[:REQUESTED]->(passwordReset)
|
||||
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
|
||||
SET passwordReset.usedAt = datetime()
|
||||
SET user.encryptedPassword = $encryptedNewPassword
|
||||
RETURN passwordReset
|
||||
`,
|
||||
{
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
},
|
||||
)
|
||||
return passwordResetTransactionResponse.records.map(record => record.get('passwordReset'))
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const response = !!(reset && reset.properties.usedAt)
|
||||
return response
|
||||
const [reset] = await passwordResetTxPromise
|
||||
return !!(reset && reset.properties.usedAt)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -14,14 +14,11 @@ let authenticatedUser
|
||||
let variables
|
||||
|
||||
const getAllPasswordResets = async () => {
|
||||
const session = driver.session()
|
||||
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()
|
||||
}
|
||||
const passwordResetQuery = await neode.cypher(
|
||||
'MATCH (passwordReset:PasswordReset) RETURN passwordReset',
|
||||
)
|
||||
const resets = passwordResetQuery.records.map(record => record.get('passwordReset'))
|
||||
return resets
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -57,17 +57,20 @@ export default {
|
||||
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||
const { postId, data } = params
|
||||
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
|
||||
`,
|
||||
const readTxResultPromise = session.readTransaction(async transaction => {
|
||||
const emotionsCountTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||
`,
|
||||
{ postId, data },
|
||||
)
|
||||
|
||||
const [emotionsCount] = transactionRes.records.map(record => {
|
||||
return record.get('emotionsCount').low
|
||||
})
|
||||
return emotionsCountTransactionResponse.records.map(
|
||||
record => record.get('emotionsCount').low,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const [emotionsCount] = await readTxResultPromise
|
||||
return emotionsCount
|
||||
} finally {
|
||||
session.close()
|
||||
@ -76,16 +79,18 @@ export default {
|
||||
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||
const { postId } = params
|
||||
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`,
|
||||
const readTxResultPromise = session.readTransaction(async transaction => {
|
||||
const emotionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
RETURN collect(emoted.emotion) as emotion
|
||||
`,
|
||||
{ userId: context.user.id, postId },
|
||||
)
|
||||
|
||||
const [emotions] = transactionRes.records.map(record => {
|
||||
return record.get('emotion')
|
||||
})
|
||||
return emotionsTransactionResponse.records.map(record => record.get('emotion'))
|
||||
})
|
||||
try {
|
||||
const [emotions] = await readTxResultPromise
|
||||
return emotions
|
||||
} finally {
|
||||
session.close()
|
||||
@ -98,25 +103,29 @@ export default {
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
params.id = params.id || uuid()
|
||||
const createPostCypher = `CREATE (post:Post {params})
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
MATCH (author:User {id: $userId})
|
||||
MERGE (post)<-[:WROTE]-(author)
|
||||
WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
RETURN post`
|
||||
|
||||
const createPostVariables = { userId: context.user.id, categoryIds, params }
|
||||
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const createPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (post:Post {params})
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
MATCH (author:User {id: $userId})
|
||||
MERGE (post)<-[:WROTE]-(author)
|
||||
WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
RETURN post
|
||||
`,
|
||||
{ userId: context.user.id, categoryIds, params },
|
||||
)
|
||||
return createPostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
try {
|
||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||
const posts = transactionRes.records.map(record => record.get('post').properties)
|
||||
return posts[0]
|
||||
const [post] = await writeTxResultPromise
|
||||
return post
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Post with this slug already exists!')
|
||||
@ -129,38 +138,44 @@ export default {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
let updatePostCypher = `
|
||||
MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
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.writeTransaction(transaction => {
|
||||
return transaction.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
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
try {
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updatePostTransactionResponse = await transaction.run(
|
||||
updatePostCypher,
|
||||
updatePostVariables,
|
||||
)
|
||||
return updatePostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
const [post] = await writeTxResultPromise
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
@ -169,23 +184,25 @@ export default {
|
||||
|
||||
DeletePost: async (object, args, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deletePostTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
SET post.content = 'UNAVAILABLE'
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
SET post.title = 'UNAVAILABLE'
|
||||
SET comment.deleted = TRUE
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
`,
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
SET post.content = 'UNAVAILABLE'
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
SET post.title = 'UNAVAILABLE'
|
||||
SET comment.deleted = TRUE
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
`,
|
||||
{ postId: args.id },
|
||||
)
|
||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||
return deletePostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
try {
|
||||
const [post] = await writeTxResultPromise
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
@ -195,21 +212,24 @@ export default {
|
||||
const { to, data } = params
|
||||
const { user } = context
|
||||
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`,
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const addPostEmotionsTransactionResponse = await transaction.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 },
|
||||
)
|
||||
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return addPostEmotionsTransactionResponse.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
...record.get('emotedRelation').properties,
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
const [emoted] = await writeTxResultPromise
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
@ -219,20 +239,25 @@ export default {
|
||||
const { to, data } = params
|
||||
const { id: from } = context.user
|
||||
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`,
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const removePostEmotionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
DELETE emotedRelation
|
||||
RETURN userFrom, postTo
|
||||
`,
|
||||
{ from, to, data },
|
||||
)
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return removePostEmotionsTransactionResponse.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
emotion: data.emotion,
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
const [emoted] = await writeTxResultPromise
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
@ -344,21 +369,28 @@ export default {
|
||||
relatedContributions: async (parent, params, context, resolveInfo) => {
|
||||
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
||||
const { id } = parent
|
||||
const statement = `
|
||||
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
WHERE NOT post.deleted AND NOT post.disabled
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
`
|
||||
let relatedContributions
|
||||
const session = context.driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const relatedContributionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
WHERE NOT post.deleted AND NOT post.disabled
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
`,
|
||||
{ id },
|
||||
)
|
||||
return relatedContributionsTransactionResponse.records.map(
|
||||
record => record.get('post').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const result = await session.run(statement, { id })
|
||||
relatedContributions = result.records.map(r => r.get('post').properties)
|
||||
const relatedContributions = await writeTxResultPromise
|
||||
return relatedContributions
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return relatedContributions
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -383,7 +383,10 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
|
||||
it('updates a post', async () => {
|
||||
const expected = { data: { UpdatePost: { id: 'p9876', content: 'New content' } } }
|
||||
const expected = {
|
||||
data: { UpdatePost: { id: 'p9876', content: 'New content' } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
@ -394,6 +397,7 @@ describe('UpdatePost', () => {
|
||||
data: {
|
||||
UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) },
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -421,6 +425,7 @@ describe('UpdatePost', () => {
|
||||
categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -441,6 +446,7 @@ describe('UpdatePost', () => {
|
||||
categories: expect.arrayContaining([{ id: 'cat27' }]),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -722,6 +728,7 @@ describe('UpdatePost', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
variables = { orderBy: ['pinned_desc', 'createdAt_desc'] }
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject(
|
||||
|
||||
@ -24,18 +24,19 @@ export default {
|
||||
const { user } = await getUserAndBadge(params)
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
// silly neode cannot remove relationships
|
||||
await session.run(
|
||||
`
|
||||
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
||||
DELETE reward
|
||||
RETURN rewardedUser
|
||||
`,
|
||||
{
|
||||
badgeKey,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
||||
DELETE reward
|
||||
RETURN rewardedUser
|
||||
`,
|
||||
{
|
||||
badgeKey,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
shout: async (_object, params, context, _resolveInfo) => {
|
||||
@ -5,22 +7,24 @@ export default {
|
||||
|
||||
const session = context.driver.session()
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
const shoutWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const shoutTransactionResponse = await transaction.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,
|
||||
},
|
||||
)
|
||||
log(shoutTransactionResponse)
|
||||
return shoutTransactionResponse.records.map(record => record.get('isShouted'))
|
||||
})
|
||||
|
||||
const [isShouted] = await shoutWriteTxResultPromise
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
@ -31,20 +35,24 @@ export default {
|
||||
const { id, type } = params
|
||||
const session = context.driver.session()
|
||||
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')
|
||||
const unshoutWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const unshoutTransactionResponse = await transaction.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,
|
||||
},
|
||||
)
|
||||
log(unshoutTransactionResponse)
|
||||
return unshoutTransactionResponse.records.map(record => record.get('isShouted'))
|
||||
})
|
||||
const [isShouted] = await unshoutWriteTxResultPromise
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
statistics: async (_parent, _args, { driver }) => {
|
||||
const session = driver.session()
|
||||
const response = {}
|
||||
const counts = {}
|
||||
try {
|
||||
const mapping = {
|
||||
countUsers: 'User',
|
||||
@ -13,27 +15,28 @@ export default {
|
||||
countFollows: 'FOLLOWS',
|
||||
countShouts: 'SHOUTED',
|
||||
}
|
||||
const cypher = `
|
||||
CALL apoc.meta.stats() YIELD labels, relTypesCount
|
||||
RETURN labels, relTypesCount
|
||||
`
|
||||
const result = await session.run(cypher)
|
||||
const [statistics] = await result.records.map(record => {
|
||||
return {
|
||||
...record.get('labels'),
|
||||
...record.get('relTypesCount'),
|
||||
}
|
||||
const statisticsReadTxResultPromise = session.readTransaction(async transaction => {
|
||||
const statisticsTransactionResponse = await transaction.run(
|
||||
`
|
||||
CALL apoc.meta.stats() YIELD labels, relTypesCount
|
||||
RETURN labels, relTypesCount
|
||||
`,
|
||||
)
|
||||
log(statisticsTransactionResponse)
|
||||
return statisticsTransactionResponse.records.map(record => {
|
||||
return {
|
||||
...record.get('labels'),
|
||||
...record.get('relTypesCount'),
|
||||
}
|
||||
})
|
||||
})
|
||||
const [statistics] = await statisticsReadTxResultPromise
|
||||
Object.keys(mapping).forEach(key => {
|
||||
const stat = statistics[mapping[key]]
|
||||
response[key] = stat ? stat.toNumber() : 0
|
||||
counts[key] = stat ? stat.toNumber() : 0
|
||||
})
|
||||
|
||||
/*
|
||||
* Note: invites count is calculated this way because invitation codes are not in use yet
|
||||
*/
|
||||
response.countInvites = response.countEmails - response.countUsers
|
||||
return response
|
||||
counts.countInvites = counts.countEmails - counts.countUsers
|
||||
return counts
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import bcrypt from 'bcryptjs'
|
||||
import { AuthenticationError } from 'apollo-server'
|
||||
import { getNeode } from '../../bootstrap/neo4j'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -25,17 +26,18 @@ export default {
|
||||
email = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
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 },
|
||||
)
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
const loginReadTxResultPromise = session.readTransaction(async transaction => {
|
||||
const loginTransactionResponse = await transaction.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 },
|
||||
)
|
||||
log(loginTransactionResponse)
|
||||
return loginTransactionResponse.records.map(record => record.get('user'))
|
||||
})
|
||||
|
||||
const [currentUser] = await loginReadTxResultPromise
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
|
||||
@ -3,6 +3,7 @@ import fileUpload from './fileUpload'
|
||||
import { getNeode } from '../../bootstrap/neo4j'
|
||||
import { UserInputError, ForbiddenError } from 'apollo-server'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -100,72 +101,89 @@ export default {
|
||||
const blockedUser = await neode.find('User', args.id)
|
||||
return blockedUser.toJson()
|
||||
},
|
||||
UpdateUser: async (object, args, context, resolveInfo) => {
|
||||
const { termsAndConditionsAgreedVersion } = args
|
||||
UpdateUser: async (_parent, params, context, _resolveInfo) => {
|
||||
const { termsAndConditionsAgreedVersion } = params
|
||||
if (termsAndConditionsAgreedVersion) {
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
throw new ForbiddenError('Invalid version format!')
|
||||
}
|
||||
args.termsAndConditionsAgreedAt = new Date().toISOString()
|
||||
params.termsAndConditionsAgreedAt = new Date().toISOString()
|
||||
}
|
||||
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
const session = context.driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $params.id})
|
||||
SET user += $params
|
||||
SET user.updatedAt = toString(datetime())
|
||||
RETURN user
|
||||
`,
|
||||
{ params },
|
||||
)
|
||||
return updateUserTransactionResponse.records.map(record => record.get('user').properties)
|
||||
})
|
||||
try {
|
||||
const user = await neode.find('User', args.id)
|
||||
if (!user) return null
|
||||
await user.update({ ...args, updatedAt: new Date().toISOString() })
|
||||
return user.toJson()
|
||||
} catch (e) {
|
||||
throw new UserInputError(e.message)
|
||||
const [user] = await writeTxResultPromise
|
||||
return user
|
||||
} catch (error) {
|
||||
throw new UserInputError(error.message)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
DeleteUser: async (object, params, context, resolveInfo) => {
|
||||
const { resource } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
let user
|
||||
try {
|
||||
if (resource && resource.length) {
|
||||
await Promise.all(
|
||||
resource.map(async node => {
|
||||
await session.run(
|
||||
await session.writeTransaction(transaction => {
|
||||
resource.map(node => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
RETURN author`,
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
RETURN author
|
||||
`,
|
||||
{
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionResult = await session.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})
|
||||
SET user.deleted = true
|
||||
SET user.name = 'UNAVAILABLE'
|
||||
SET user.about = 'UNAVAILABLE'
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
|
||||
DETACH DELETE email
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
|
||||
DETACH DELETE socialMedia
|
||||
RETURN user`,
|
||||
{ userId: context.user.id },
|
||||
)
|
||||
user = transactionResult.records.map(r => r.get('user').properties)[0]
|
||||
const deleteUserTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deleteUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})
|
||||
SET user.deleted = true
|
||||
SET user.name = 'UNAVAILABLE'
|
||||
SET user.about = 'UNAVAILABLE'
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
|
||||
DETACH DELETE email
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
|
||||
DETACH DELETE socialMedia
|
||||
RETURN user
|
||||
`,
|
||||
{ userId: context.user.id },
|
||||
)
|
||||
log(deleteUserTransactionResponse)
|
||||
return deleteUserTransactionResponse.records.map(record => record.get('user').properties)
|
||||
})
|
||||
const [user] = await deleteUserTxResultPromise
|
||||
return user
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return user
|
||||
},
|
||||
},
|
||||
User: {
|
||||
|
||||
@ -68,6 +68,7 @@ describe('User', () => {
|
||||
it('is permitted', async () => {
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
data: { User: [{ name: 'Johnny' }] },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@ -90,8 +91,7 @@ describe('User', () => {
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
let userParams
|
||||
let variables
|
||||
let userParams, variables
|
||||
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
@ -111,16 +111,23 @@ describe('UpdateUser', () => {
|
||||
})
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String, $termsAndConditionsAgreedVersion: String) {
|
||||
mutation(
|
||||
$id: ID!
|
||||
$name: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$locationName: String
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
name: $name
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -152,7 +159,7 @@ describe('UpdateUser', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('name within specifications', async () => {
|
||||
it('updates the name', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
UpdateUser: {
|
||||
@ -160,36 +167,13 @@ describe('UpdateUser', () => {
|
||||
name: 'John Doughnut',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('with `null` as name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: null,
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty(
|
||||
'message',
|
||||
'child "name" fails because ["name" contains an invalid value, "name" must be a string]',
|
||||
)
|
||||
})
|
||||
|
||||
it('with too short name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: ' ',
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty(
|
||||
'message',
|
||||
'child "name" fails because ["name" length must be at least 3 characters long]',
|
||||
)
|
||||
})
|
||||
|
||||
describe('given a new agreed version of terms and conditions', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' }
|
||||
@ -202,6 +186,7 @@ describe('UpdateUser', () => {
|
||||
termsAndConditionsAgreedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
@ -222,6 +207,7 @@ describe('UpdateUser', () => {
|
||||
termsAndConditionsAgreedAt: null,
|
||||
}),
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
@ -238,6 +224,14 @@ describe('UpdateUser', () => {
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Invalid version format!')
|
||||
})
|
||||
|
||||
it('supports updating location', async () => {
|
||||
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States of America' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -372,6 +366,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
|
||||
expectedResponse,
|
||||
@ -418,6 +413,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
@ -465,6 +461,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
@ -511,6 +508,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
|
||||
@ -26,7 +26,7 @@ enum _UserOrdering {
|
||||
type User {
|
||||
id: ID!
|
||||
actorId: String
|
||||
name: String
|
||||
name: String!
|
||||
email: String! @cypher(statement: "MATCH (this)-[: PRIMARY_EMAIL]->(e: EmailAddress) RETURN e.email")
|
||||
slug: String!
|
||||
avatar: String
|
||||
|
||||
@ -29,10 +29,16 @@ const factories = {
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const cypher = 'MATCH (n) DETACH DELETE n'
|
||||
const session = driver.session()
|
||||
try {
|
||||
return await session.run(cypher)
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (everything)
|
||||
DETACH DELETE everything
|
||||
`,
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { allowEmbedIframesMutation } from '~/graphql/User.js'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
name: 'embed-component',
|
||||
@ -129,7 +129,7 @@ export default {
|
||||
async updateEmbedSettings(allowEmbedIframes) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: allowEmbedIframesMutation(),
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
allowEmbedIframes,
|
||||
|
||||
@ -33,12 +33,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import find from 'lodash/find'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import locales from '~/locales'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -87,14 +87,7 @@ export default {
|
||||
if (!this.currentUser || !this.currentUser.id) return null
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($id: ID!, $locale: String) {
|
||||
UpdateUser(id: $id, locale: $locale) {
|
||||
id
|
||||
locale
|
||||
}
|
||||
}
|
||||
`,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
locale: this.$i18n.locale(),
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import vueDropzone from 'nuxt-dropzone'
|
||||
import gql from 'graphql-tag'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -62,14 +62,7 @@ export default {
|
||||
const avatarUpload = file[0]
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation($id: ID!, $avatarUpload: Upload) {
|
||||
UpdateUser(id: $id, avatarUpload: $avatarUpload) {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
`,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
avatarUpload,
|
||||
id: this.user.id,
|
||||
|
||||
@ -140,23 +140,42 @@ export const unfollowUserMutation = i18n => {
|
||||
`
|
||||
}
|
||||
|
||||
export const allowEmbedIframesMutation = () => {
|
||||
export const updateUserMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $allowEmbedIframes: Boolean) {
|
||||
UpdateUser(id: $id, allowEmbedIframes: $allowEmbedIframes) {
|
||||
mutation(
|
||||
$id: ID!
|
||||
$slug: String
|
||||
$name: String
|
||||
$locationName: String
|
||||
$about: String
|
||||
$allowEmbedIframes: Boolean
|
||||
$showShoutsPublicly: Boolean
|
||||
$locale: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$avatarUpload: Upload
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
slug: $slug
|
||||
name: $name
|
||||
locationName: $locationName
|
||||
about: $about
|
||||
allowEmbedIframes: $allowEmbedIframes
|
||||
showShoutsPublicly: $showShoutsPublicly
|
||||
locale: $locale
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
avatarUpload: $avatarUpload
|
||||
) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
locationName
|
||||
about
|
||||
allowEmbedIframes
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const showShoutsPubliclyMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $showShoutsPublicly: Boolean) {
|
||||
UpdateUser(id: $id, showShoutsPublicly: $showShoutsPublicly) {
|
||||
id
|
||||
showShoutsPublicly
|
||||
locale
|
||||
termsAndConditionsAgreedVersion
|
||||
avatar
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -169,14 +188,3 @@ export const checkSlugAvailableQuery = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const localeMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $locale: String) {
|
||||
UpdateUser(id: $id, locale: $locale) {
|
||||
id
|
||||
locale
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { allowEmbedIframesMutation } from '~/graphql/User.js'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
head() {
|
||||
@ -69,7 +69,7 @@ export default {
|
||||
async submit() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: allowEmbedIframesMutation(),
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
allowEmbedIframes: !this.disabled,
|
||||
|
||||
@ -41,40 +41,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { CancelToken } from 'axios'
|
||||
import UniqueSlugForm from '~/components/utils/UniqueSlugForm'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
|
||||
let timeout
|
||||
const mapboxToken = process.env.MAPBOX_TOKEN
|
||||
|
||||
/*
|
||||
const query = gql`
|
||||
query getUser($id: ID) {
|
||||
User(id: $id) {
|
||||
id
|
||||
name
|
||||
locationName
|
||||
about
|
||||
}
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $slug: String, $name: String, $locationName: String, $about: String) {
|
||||
UpdateUser(id: $id, slug: $slug, name: $name, locationName: $locationName, about: $about) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
locationName
|
||||
about
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@ -120,7 +94,7 @@ export default {
|
||||
locationName = locationName && (locationName.label || locationName)
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
name,
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { showShoutsPubliclyMutation } from '~/graphql/User'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@ -36,7 +36,7 @@ export default {
|
||||
async submit() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: showShoutsPubliclyMutation(),
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
showShoutsPublicly: this.shoutsAllowed,
|
||||
|
||||
@ -24,17 +24,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { VERSION } from '~/constants/terms-and-conditions-version.js'
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $termsAndConditionsAgreedVersion: String) {
|
||||
UpdateUser(id: $id, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) {
|
||||
id
|
||||
termsAndConditionsAgreedVersion
|
||||
}
|
||||
}
|
||||
`
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
layout: 'default',
|
||||
head() {
|
||||
@ -74,7 +67,7 @@ export default {
|
||||
async submit() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
termsAndConditionsAgreedVersion: VERSION,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user