Merge branch 'master' into C-1187-terms-and-conditions-confirmed-function

This commit is contained in:
Alexander Friedland 2019-09-03 09:51:12 +02:00 committed by GitHub
commit 41767cc27e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1557 additions and 1596 deletions

View File

@ -49,7 +49,7 @@
"apollo-client": "~2.6.4", "apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.18", "apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15", "apollo-link-http": "~1.5.15",
"apollo-server": "~2.9.1", "apollo-server": "~2.9.3",
"apollo-server-express": "^2.9.0", "apollo-server-express": "^2.9.0",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
@ -61,7 +61,7 @@
"dotenv": "~8.1.0", "dotenv": "~8.1.0",
"express": "^4.17.1", "express": "^4.17.1",
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",
"graphql": "^14.5.3", "graphql": "^14.5.4",
"graphql-custom-directives": "~0.2.14", "graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1", "graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.5", "graphql-middleware": "~3.0.5",
@ -90,7 +90,7 @@
"metascraper-video": "^5.6.5", "metascraper-video": "^5.6.5",
"metascraper-youtube": "^5.6.3", "metascraper-youtube": "^5.6.3",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"neo4j-driver": "~1.7.5", "neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.7.2", "neo4j-graphql-js": "^2.7.2",
"neode": "^0.3.2", "neode": "^0.3.2",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
@ -116,12 +116,12 @@
"babel-jest": "~24.9.0", "babel-jest": "~24.9.0",
"chai": "~4.2.0", "chai": "~4.2.0",
"cucumber": "~5.1.0", "cucumber": "~5.1.0",
"eslint": "~6.2.2", "eslint": "~6.3.0",
"eslint-config-prettier": "~6.1.0", "eslint-config-prettier": "~6.1.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2", "eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.2", "eslint-plugin-jest": "~22.16.0",
"eslint-plugin-node": "~9.1.0", "eslint-plugin-node": "~9.2.0",
"eslint-plugin-prettier": "~3.1.0", "eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1", "eslint-plugin-standard": "~4.0.1",

View File

@ -12,11 +12,9 @@ export default {
CreatePost: setCreatedAt, CreatePost: setCreatedAt,
CreateComment: setCreatedAt, CreateComment: setCreatedAt,
CreateOrganization: setCreatedAt, CreateOrganization: setCreatedAt,
CreateNotification: setCreatedAt,
UpdateUser: setUpdatedAt, UpdateUser: setUpdatedAt,
UpdatePost: setUpdatedAt, UpdatePost: setUpdatedAt,
UpdateComment: setUpdatedAt, UpdateComment: setUpdatedAt,
UpdateOrganization: setUpdatedAt, UpdateOrganization: setUpdatedAt,
UpdateNotification: setUpdatedAt,
}, },
} }

View File

@ -4,13 +4,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
if (!idsOfUsers.length) return if (!idsOfUsers.length) return
// Checked here, because it does not go through GraphQL checks at all in this file. // Checked here, because it does not go through GraphQL checks at all in this file.
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'] const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
if (!reasonsAllowed.includes(reason)) { if (!reasonsAllowed.includes(reason)) {
throw new Error('Notification reason is not allowed!') throw new Error('Notification reason is not allowed!')
} }
if ( if (
(label === 'Post' && reason !== 'mentioned_in_post') || (label === 'Post' && reason !== 'mentioned_in_post') ||
(label === 'Comment' && !['mentioned_in_comment', 'comment_on_post'].includes(reason)) (label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
) { ) {
throw new Error('Notification does not fit the reason!') throw new Error('Notification does not fit the reason!')
} }
@ -25,8 +25,9 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
MATCH (user: User) MATCH (user: User)
WHERE user.id in $idsOfUsers WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author) AND NOT (user)<-[:BLOCKED]-(author)
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) SET notification.read = FALSE
SET notification.createdAt = $createdAt
` `
break break
} }
@ -37,20 +38,22 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
WHERE user.id in $idsOfUsers WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author) AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (user)<-[:BLOCKED]-(postAuthor) AND NOT (user)<-[:BLOCKED]-(postAuthor)
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) SET notification.read = FALSE
SET notification.createdAt = $createdAt
` `
break break
} }
case 'comment_on_post': { case 'commented_on_post': {
cypher = ` cypher = `
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
MATCH (user: User) MATCH (user: User)
WHERE user.id in $idsOfUsers WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author) AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (author)<-[:BLOCKED]-(user) AND NOT (author)<-[:BLOCKED]-(user)
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) SET notification.read = FALSE
SET notification.createdAt = $createdAt
` `
break break
} }
@ -105,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
return record.get('user') return record.get('user')
}) })
if (context.user.id !== postAuthor.id) { if (context.user.id !== postAuthor.id) {
await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_post', context) await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
} }
} }

View File

@ -77,14 +77,18 @@ afterEach(async () => {
describe('notifications', () => { describe('notifications', () => {
const notificationQuery = gql` const notificationQuery = gql`
query($read: Boolean) { query($read: Boolean) {
currentUser { notifications(read: $read, orderBy: createdAt_desc) {
notifications(read: $read, orderBy: createdAt_desc) { read
read reason
reason createdAt
post { from {
__typename
... on Post {
id
content content
} }
comment { ... on Comment {
id
content content
} }
} }
@ -154,18 +158,18 @@ describe('notifications', () => {
await createCommentOnPostAction() await createCommentOnPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: {
currentUser: { notifications: [
notifications: [ {
{ read: false,
read: false, createdAt: expect.any(String),
reason: 'comment_on_post', reason: 'commented_on_post',
post: null, from: {
comment: { __typename: 'Comment',
content: commentContent, id: 'c47',
}, content: commentContent,
}, },
], },
}, ],
}, },
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
@ -183,11 +187,7 @@ describe('notifications', () => {
await notifiedUser.relateTo(commentAuthor, 'blocked') await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction() await createCommentOnPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: { notifications: [] },
currentUser: {
notifications: [],
},
},
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect( await expect(
@ -211,11 +211,7 @@ describe('notifications', () => {
await notifiedUser.relateTo(commentAuthor, 'blocked') await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction() await createCommentOnPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: { notifications: [] },
currentUser: {
notifications: [],
},
},
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect( await expect(
@ -253,18 +249,18 @@ describe('notifications', () => {
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?' 'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: {
currentUser: { notifications: [
notifications: [ {
{ read: false,
read: false, createdAt: expect.any(String),
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
post: { from: {
content: expectedContent, __typename: 'Post',
}, id: 'p47',
comment: null, content: expectedContent,
}, },
], },
}, ],
}, },
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
@ -278,7 +274,7 @@ describe('notifications', () => {
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
describe('many times', () => { describe('updates the post and mentions me again', () => {
const updatePostAction = async () => { const updatePostAction = async () => {
const updatedContent = ` const updatedContent = `
One more mention to One more mention to
@ -307,33 +303,25 @@ describe('notifications', () => {
authenticatedUser = await notifiedUser.toJson() authenticatedUser = await notifiedUser.toJson()
} }
it('creates exactly one more notification', async () => { it('creates no duplicate notification for the same resource', async () => {
const expectedUpdatedContent =
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
await createPostAction() await createPostAction()
await updatePostAction() await updatePostAction()
const expectedContent =
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: {
currentUser: { notifications: [
notifications: [ {
{ read: false,
read: false, createdAt: expect.any(String),
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
post: { from: {
content: expectedContent, __typename: 'Post',
}, id: 'p47',
comment: null, content: expectedUpdatedContent,
}, },
{ },
read: false, ],
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
},
],
},
}, },
}) })
await expect( await expect(
@ -345,6 +333,68 @@ describe('notifications', () => {
}), }),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
describe('if the notification was marked as read earlier', () => {
const markAsReadAction = async () => {
const mutation = gql`
mutation($id: ID!) {
markAsRead(id: $id) {
read
}
}
`
await mutate({ mutation, variables: { id: 'p47' } })
}
describe('but the next mention happens after the notification was marked as read', () => {
it('sets the `read` attribute to false again', async () => {
await createPostAction()
await markAsReadAction()
const {
data: {
notifications: [{ read: readBefore }],
},
} = await query({
query: notificationQuery,
})
await updatePostAction()
const {
data: {
notifications: [{ read: readAfter }],
},
} = await query({
query: notificationQuery,
})
expect(readBefore).toEqual(true)
expect(readAfter).toEqual(false)
})
it('updates the `createdAt` attribute', async () => {
await createPostAction()
await markAsReadAction()
const {
data: {
notifications: [{ createdAt: createdAtBefore }],
},
} = await query({
query: notificationQuery,
})
await updatePostAction()
const {
data: {
notifications: [{ createdAt: createdAtAfter }],
},
} = await query({
query: notificationQuery,
})
expect(createdAtBefore).toBeTruthy()
expect(Date.parse(createdAtBefore)).toEqual(expect.any(Number))
expect(createdAtAfter).toBeTruthy()
expect(Date.parse(createdAtAfter)).toEqual(expect.any(Number))
expect(createdAtBefore).not.toEqual(createdAtAfter)
})
})
})
}) })
describe('but the author of the post blocked me', () => { describe('but the author of the post blocked me', () => {
@ -355,11 +405,7 @@ describe('notifications', () => {
it('sends no notification', async () => { it('sends no notification', async () => {
await createPostAction() await createPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: { notifications: [] },
currentUser: {
notifications: [],
},
},
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect( await expect(
@ -397,18 +443,18 @@ describe('notifications', () => {
await createCommentOnPostAction() await createCommentOnPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: {
currentUser: { notifications: [
notifications: [ {
{ read: false,
read: false, createdAt: expect.any(String),
reason: 'mentioned_in_comment', reason: 'mentioned_in_comment',
post: null, from: {
comment: { __typename: 'Comment',
content: commentContent, id: 'c47',
}, content: commentContent,
}, },
], },
}, ],
}, },
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
@ -440,11 +486,7 @@ describe('notifications', () => {
it('sends no notification', async () => { it('sends no notification', async () => {
await createCommentOnPostAction() await createCommentOnPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: { notifications: [] },
currentUser: {
notifications: [],
},
},
}) })
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect( await expect(

View File

@ -41,32 +41,6 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id return socialMedia.ownedBy.node.id === user.id
}) })
const belongsToMe = rule({
cache: 'no_cache',
})(async (_, args, context) => {
const {
driver,
user: { id: userId },
} = context
const { id: notificationId } = args
const session = driver.session()
const result = await session.run(
`
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
RETURN n
`,
{
userId,
notificationId,
},
)
const [notification] = result.records.map(record => {
return record.get('n')
})
session.close()
return Boolean(notification)
})
/* TODO: decide if we want to remove this check: the check /* TODO: decide if we want to remove this check: the check
* `onlyEnabledContent` throws authorization errors only if you have * `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter * arguments for `disabled` or `deleted` assuming these are filter
@ -149,7 +123,6 @@ const permissions = shield(
Category: allow, Category: allow,
Tag: allow, Tag: allow,
Report: isModerator, Report: isModerator,
Notification: isAdmin,
statistics: allow, statistics: allow,
currentUser: allow, currentUser: allow,
Post: or(onlyEnabledContent, isModerator), Post: or(onlyEnabledContent, isModerator),
@ -160,6 +133,7 @@ const permissions = shield(
PostsEmotionsCountByEmotion: allow, PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: allow, PostsEmotionsByCurrentUser: allow,
blockedUsers: isAuthenticated, blockedUsers: isAuthenticated,
notifications: isAuthenticated,
}, },
Mutation: { Mutation: {
'*': deny, '*': deny,
@ -168,7 +142,6 @@ const permissions = shield(
Signup: isAdmin, Signup: isAdmin,
SignupVerification: allow, SignupVerification: allow,
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)), CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
UpdateNotification: belongsToMe,
UpdateUser: onlyYourself, UpdateUser: onlyYourself,
CreatePost: isAuthenticated, CreatePost: isAuthenticated,
UpdatePost: isAuthor, UpdatePost: isAuthor,
@ -198,6 +171,7 @@ const permissions = shield(
RemovePostEmotions: isAuthenticated, RemovePostEmotions: isAuthenticated,
block: isAuthenticated, block: isAuthenticated,
unblock: isAuthenticated, unblock: isAuthenticated,
markAsRead: isAuthenticated,
}, },
User: { User: {
email: isMyOwn, email: isMyOwn,

View File

@ -1,36 +0,0 @@
import uuid from 'uuid/v4'
module.exports = {
id: {
type: 'uuid',
primary: true,
default: uuid,
},
read: {
type: 'boolean',
default: false,
},
reason: {
type: 'string',
valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'],
invalid: [null],
default: 'mentioned_in_post',
},
createdAt: {
type: 'string',
isoDate: true,
default: () => new Date().toISOString(),
},
user: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'User',
direction: 'out',
},
post: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'Post',
direction: 'in',
},
}

View File

@ -7,6 +7,5 @@ export default {
EmailAddress: require('./EmailAddress.js'), EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'), SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'), Post: require('./Post.js'),
Notification: require('./Notification.js'),
Category: require('./Category.js'), Category: require('./Category.js'),
} }

View File

@ -20,6 +20,7 @@ export default applyScalars(
'Statistics', 'Statistics',
'LoggedInUser', 'LoggedInUser',
'SocialMedia', 'SocialMedia',
'NOTIFIED',
], ],
// add 'User' here as soon as possible // add 'User' here as soon as possible
}, },
@ -32,6 +33,7 @@ export default applyScalars(
'Statistics', 'Statistics',
'LoggedInUser', 'LoggedInUser',
'SocialMedia', 'SocialMedia',
'NOTIFIED',
], ],
// add 'User' here as soon as possible // add 'User' here as soon as possible
}, },

View File

@ -1,4 +1,5 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import Resolver from './helpers/Resolver'
export default { export default {
Mutation: { Mutation: {
@ -52,4 +53,13 @@ export default {
return comment return comment
}, },
}, },
Comment: {
...Resolver('Comment', {
hasOne: {
author: '<-[:WROTE]-(related:User)',
post: '-[:COMMENTS]->(related:Post)',
disabledBy: '<-[:DISABLED]-(related:User)',
},
}),
},
} }

View File

@ -61,7 +61,6 @@ export default function Resolver(type, options = {}) {
const id = parent[idAttribute] const id = parent[idAttribute]
const statement = ` const statement = `
MATCH(u:${type} {${idAttribute}: {id}})${connection} MATCH(u:${type} {${idAttribute}: {id}})${connection}
WHERE NOT related.deleted = true AND NOT related.disabled = true
RETURN COUNT(DISTINCT(related)) as count RETURN COUNT(DISTINCT(related)) as count
` `
const result = await instance.cypher(statement, { id }) const result = await instance.cypher(statement, { id })

View File

@ -1,14 +1,80 @@
import { neo4jgraphql } from 'neo4j-graphql-js' const resourceTypes = ['Post', 'Comment']
const transformReturnType = record => {
return {
...record.get('notification').properties,
from: {
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
...record.get('resource').properties,
},
to: {
...record.get('user').properties,
},
}
}
export default { export default {
Query: { Query: {
Notification: (object, params, context, resolveInfo) => { notifications: async (parent, args, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false) const { user: currentUser } = context
const session = context.driver.session()
let notifications
let whereClause
let orderByClause
switch (args.read) {
case true:
whereClause = 'WHERE notification.read = TRUE'
break
case false:
whereClause = 'WHERE notification.read = FALSE'
break
default:
whereClause = ''
}
switch (args.orderBy) {
case 'createdAt_asc':
orderByClause = 'ORDER BY notification.createdAt ASC'
break
case 'createdAt_desc':
orderByClause = 'ORDER BY notification.createdAt DESC'
break
default:
orderByClause = ''
}
try {
const cypher = `
MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id})
${whereClause}
RETURN resource, notification, user
${orderByClause}
`
const result = await session.run(cypher, { id: currentUser.id })
notifications = await result.records.map(transformReturnType)
} finally {
session.close()
}
return notifications
}, },
}, },
Mutation: { Mutation: {
UpdateNotification: (object, params, context, resolveInfo) => { markAsRead: async (parent, args, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false) 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})
SET notification.read = TRUE
RETURN resource, notification, user
`
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
const notifications = await result.records.map(transformReturnType)
notification = notifications[0]
} finally {
session.close()
}
return notification
}, },
}, },
} }

View File

@ -1,397 +1,309 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers' import { gql } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j' import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
let client
const factory = Factory() const factory = Factory()
const instance = neode() const neode = getNeode()
const driver = getDriver()
const userParams = { const userParams = {
id: 'you', id: 'you',
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
} }
const categoryIds = ['cat9']
let authenticatedUser
let user
let variables
let query
let mutate
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
})
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', userParams) authenticatedUser = null
await instance.create('Category', { variables = { orderBy: 'createdAt_asc' }
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
}) })
afterEach(async () => { afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('Notification', () => { describe('given some notifications', () => {
const notificationQuery = gql` beforeEach(async () => {
query { user = await factory.create('User', userParams)
Notification { await factory.create('User', { id: 'neighbor' })
id await Promise.all(setupNotifications.map(s => neode.cypher(s)))
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised')
})
}) })
}) const setupNotifications = [
`MATCH(user:User {id: 'neighbor'})
MERGE (:Post {id: 'p1', content: 'Not for you'})
-[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'})
-[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'})
-[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-08-30T19:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`,
`MATCH(user:User {id: 'neighbor'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`,
]
describe('currentUser notifications', () => { describe('notifications', () => {
const variables = {} const notificationQuery = gql`
query($read: Boolean, $orderBy: NotificationOrdering) {
describe('authenticated', () => { notifications(read: $read, orderBy: $orderBy) {
let headers from {
beforeEach(async () => { __typename
headers = await login({ ... on Post {
email: 'test@example.org', content
password: '1234', }
}) ... on Comment {
client = new GraphQLClient(host, { content
headers, }
}) }
}) read
createdAt
describe('given some notifications', () => {
beforeEach(async () => {
const neighborParams = {
email: 'neighbor@example.org',
password: '1234',
id: 'neighbor',
} }
await Promise.all([
factory.create('User', neighborParams),
factory.create('Notification', {
id: 'post-mention-not-for-you',
reason: 'mentioned_in_post',
}),
factory.create('Notification', {
id: 'post-mention-already-seen',
read: true,
reason: 'mentioned_in_post',
}),
factory.create('Notification', {
id: 'post-mention-unseen',
reason: 'mentioned_in_post',
}),
factory.create('Notification', {
id: 'comment-mention-not-for-you',
reason: 'mentioned_in_comment',
}),
factory.create('Notification', {
id: 'comment-mention-already-seen',
read: true,
reason: 'mentioned_in_comment',
}),
factory.create('Notification', {
id: 'comment-mention-unseen',
reason: 'mentioned_in_comment',
}),
])
await factory.authenticateAs(neighborParams)
await factory.create('Post', { id: 'p1', categoryIds })
await Promise.all([
factory.relate('Notification', 'User', {
from: 'post-mention-not-for-you',
to: 'neighbor',
}),
factory.relate('Notification', 'Post', {
from: 'p1',
to: 'post-mention-not-for-you',
}),
factory.relate('Notification', 'User', {
from: 'post-mention-unseen',
to: 'you',
}),
factory.relate('Notification', 'Post', {
from: 'p1',
to: 'post-mention-unseen',
}),
factory.relate('Notification', 'User', {
from: 'post-mention-already-seen',
to: 'you',
}),
factory.relate('Notification', 'Post', {
from: 'p1',
to: 'post-mention-already-seen',
}),
])
// Comment and its notifications
await Promise.all([
factory.create('Comment', {
id: 'c1',
postId: 'p1',
}),
])
await Promise.all([
factory.relate('Notification', 'User', {
from: 'comment-mention-not-for-you',
to: 'neighbor',
}),
factory.relate('Notification', 'Comment', {
from: 'c1',
to: 'comment-mention-not-for-you',
}),
factory.relate('Notification', 'User', {
from: 'comment-mention-unseen',
to: 'you',
}),
factory.relate('Notification', 'Comment', {
from: 'c1',
to: 'comment-mention-unseen',
}),
factory.relate('Notification', 'User', {
from: 'comment-mention-already-seen',
to: 'you',
}),
factory.relate('Notification', 'Comment', {
from: 'c1',
to: 'comment-mention-already-seen',
}),
])
})
describe('filter for read: false', () => {
const queryCurrentUserNotificationsFilterRead = gql`
query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
id
post {
id
}
comment {
id
}
}
}
}
`
const variables = { read: false }
it('returns only unread notifications of current user', async () => {
const expected = {
currentUser: {
notifications: expect.arrayContaining([
{
id: 'post-mention-unseen',
post: {
id: 'p1',
},
comment: null,
},
{
id: 'comment-mention-unseen',
post: null,
comment: {
id: 'c1',
},
},
]),
},
}
await expect(
client.request(queryCurrentUserNotificationsFilterRead, variables),
).resolves.toEqual(expected)
})
})
describe('no filters', () => {
const queryCurrentUserNotifications = gql`
query {
currentUser {
notifications(orderBy: createdAt_desc) {
id
post {
id
}
comment {
id
}
}
}
}
`
it('returns all notifications of current user', async () => {
const expected = {
currentUser: {
notifications: expect.arrayContaining([
{
id: 'post-mention-unseen',
post: {
id: 'p1',
},
comment: null,
},
{
id: 'post-mention-already-seen',
post: {
id: 'p1',
},
comment: null,
},
{
id: 'comment-mention-unseen',
comment: {
id: 'c1',
},
post: null,
},
{
id: 'comment-mention-already-seen',
comment: {
id: 'c1',
},
post: null,
},
]),
},
}
await expect(client.request(queryCurrentUserNotifications, variables)).resolves.toEqual(
expected,
)
})
})
})
})
})
describe('UpdateNotification', () => {
const mutationUpdateNotification = gql`
mutation($id: ID!, $read: Boolean) {
UpdateNotification(id: $id, read: $read) {
id
read
} }
} `
`
const variablesPostUpdateNotification = {
id: 'post-mention-to-be-updated',
read: true,
}
const variablesCommentUpdateNotification = {
id: 'comment-mention-to-be-updated',
read: true,
}
describe('given some notifications', () => {
let headers
beforeEach(async () => {
const mentionedParams = {
id: 'mentioned-1',
email: 'mentioned@example.org',
password: '1234',
slug: 'mentioned',
}
await Promise.all([
factory.create('User', mentionedParams),
factory.create('Notification', {
id: 'post-mention-to-be-updated',
reason: 'mentioned_in_post',
}),
factory.create('Notification', {
id: 'comment-mention-to-be-updated',
reason: 'mentioned_in_comment',
}),
])
await factory.authenticateAs(userParams)
await factory.create('Post', { id: 'p1', categoryIds })
await Promise.all([
factory.relate('Notification', 'User', {
from: 'post-mention-to-be-updated',
to: 'mentioned-1',
}),
factory.relate('Notification', 'Post', {
from: 'p1',
to: 'post-mention-to-be-updated',
}),
])
// Comment and its notifications
await Promise.all([
factory.create('Comment', {
id: 'c1',
postId: 'p1',
}),
])
await Promise.all([
factory.relate('Notification', 'User', {
from: 'comment-mention-to-be-updated',
to: 'mentioned-1',
}),
factory.relate('Notification', 'Comment', {
from: 'p1',
to: 'comment-mention-to-be-updated',
}),
])
})
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) const result = await query({ query: notificationQuery })
await expect( expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
).rejects.toThrow('Not Authorised')
}) })
}) })
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ authenticatedUser = await user.toJson()
email: 'test@example.org', })
password: '1234',
}) describe('no filters', () => {
client = new GraphQLClient(host, { it('returns all notifications of current user', async () => {
headers, const expected = expect.objectContaining({
data: {
notifications: [
{
from: {
__typename: 'Comment',
content: 'You have seen this comment mentioning already',
},
read: true,
createdAt: '2019-08-30T15:33:48.651Z',
},
{
from: {
__typename: 'Post',
content: 'Already seen post mentioning',
},
read: true,
createdAt: '2019-08-30T17:33:48.651Z',
},
{
from: {
__typename: 'Comment',
content: 'You have been mentioned in a comment',
},
read: false,
createdAt: '2019-08-30T19:33:48.651Z',
},
{
from: {
__typename: 'Post',
content: 'You have been mentioned in a post',
},
read: false,
createdAt: '2019-08-31T17:33:48.651Z',
},
],
},
})
await expect(query({ query: notificationQuery, variables })).resolves.toEqual(expected)
}) })
}) })
it('throws authorization error', async () => { describe('filter for read: false', () => {
await expect( it('returns only unread notifications of current user', async () => {
client.request(mutationUpdateNotification, variablesPostUpdateNotification), const expected = expect.objectContaining({
).rejects.toThrow('Not Authorised') data: {
}) notifications: [
{
describe('and owner', () => { from: {
beforeEach(async () => { __typename: 'Comment',
headers = await login({ content: 'You have been mentioned in a comment',
email: 'mentioned@example.org', },
password: '1234', read: false,
}) createdAt: '2019-08-30T19:33:48.651Z',
client = new GraphQLClient(host, { },
headers, {
}) from: {
}) __typename: 'Post',
content: 'You have been mentioned in a post',
it('updates post notification', async () => { },
const expected = { read: false,
UpdateNotification: { createdAt: '2019-08-31T17:33:48.651Z',
id: 'post-mention-to-be-updated', },
read: true, ],
}, },
} })
await expect( await expect(
client.request(mutationUpdateNotification, variablesPostUpdateNotification), query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toEqual(expected)
})
it('updates comment notification', async () => {
const expected = {
UpdateNotification: {
id: 'comment-mention-to-be-updated',
read: true,
},
}
await expect(
client.request(mutationUpdateNotification, variablesCommentUpdateNotification),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
}) })
}) })
}) })
describe('markAsRead', () => {
const markAsReadMutation = gql`
mutation($id: ID!) {
markAsRead(id: $id) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const result = await mutate({
mutation: markAsReadMutation,
variables: { ...variables, id: 'p1' },
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
describe('not being notified at all', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p1',
}
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
describe('being notified', () => {
describe('on a post', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p3',
}
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
expect(data).toEqual({
markAsRead: {
from: {
__typename: 'Post',
content: 'You have been mentioned in a post',
},
read: true,
createdAt: '2019-08-31T17:33:48.651Z',
},
})
})
describe('but notification was already marked as read', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p2',
}
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
})
describe('on a comment', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'c2',
}
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
expect(data).toEqual({
markAsRead: {
from: {
__typename: 'Comment',
content: 'You have been mentioned in a comment',
},
read: true,
createdAt: '2019-08-30T19:33:48.651Z',
},
})
})
})
})
})
})
}) })

View File

@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { getBlockedUsers, getBlockedByUsers } from './users.js' import { getBlockedUsers, getBlockedByUsers } from './users.js'
import { mergeWith, isArray } from 'lodash' import { mergeWith, isArray } from 'lodash'
import Resolver from './helpers/Resolver'
const filterForBlockedUsers = async (params, context) => { const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params if (!context.user) return params
@ -11,6 +12,8 @@ const filterForBlockedUsers = async (params, context) => {
getBlockedByUsers(context), getBlockedByUsers(context),
]) ])
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)] const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
if (!badIds.length) return params
params.filter = mergeWith( params.filter = mergeWith(
params.filter, params.filter,
{ {
@ -179,4 +182,46 @@ export default {
return emoted return emoted
}, },
}, },
Post: {
...Resolver('Post', {
hasMany: {
tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)',
comments: '<-[:COMMENTS]-(related:Comment)',
shoutedBy: '<-[:SHOUTED]-(related:User)',
emotions: '<-[related:EMOTED]',
},
hasOne: {
author: '<-[:WROTE]-(related:User)',
disabledBy: '<-[:DISABLED]-(related:User)',
},
count: {
shoutedCount:
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
emotionsCount: '<-[related:EMOTED]-(:User)',
},
boolean: {
shoutedByCurrentUser:
'<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},
}),
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)
RETURN DISTINCT post
LIMIT 10
`
let relatedContributions
const session = context.driver.session()
try {
const result = await session.run(statement, { id })
relatedContributions = result.records.map(r => r.get('post').properties)
} finally {
session.close()
}
return relatedContributions
},
},
} }

View File

@ -210,12 +210,15 @@ export default {
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', 'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
}, },
count: { count: {
contributionsCount: '-[:WROTE]->(related:Post)', contributionsCount:
'-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
friendsCount: '<-[:FRIENDS]->(related:User)', friendsCount: '<-[:FRIENDS]->(related:User)',
followingCount: '-[:FOLLOWS]->(related:User)', followingCount: '-[:FOLLOWS]->(related:User)',
followedByCount: '<-[:FOLLOWS]-(related:User)', followedByCount: '<-[:FOLLOWS]-(related:User)',
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)', commentedCount:
shoutedCount: '-[:SHOUTED]->(related:Post)', '-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
shoutedCount:
'-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
badgesCount: '<-[:REWARDED]-(related:Badge)', badgesCount: '<-[:REWARDED]-(related:Badge)',
}, },
hasOne: { hasOne: {

View File

@ -1,5 +1,5 @@
enum ReasonNotification { enum ReasonNotification {
mentioned_in_post mentioned_in_post
mentioned_in_comment mentioned_in_comment
comment_on_post commented_on_post
} }

View File

@ -4,7 +4,7 @@ type Query {
currentUser: User currentUser: User
# Get the latest Network Statistics # Get the latest Network Statistics
statistics: Statistics! statistics: Statistics!
findPosts(query: String!, limit: Int = 10): [Post]! findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher( @cypher(
statement: """ statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $query) CALL db.index.fulltext.queryNodes('full_text_search', $query)

View File

@ -0,0 +1,28 @@
type NOTIFIED {
from: NotificationSource
to: User
createdAt: String
read: Boolean
reason: NotificationReason
}
union NotificationSource = Post | Comment
enum NotificationOrdering {
createdAt_asc
createdAt_desc
}
enum NotificationReason {
mentioned_in_post
mentioned_in_comment
commented_on_post
}
type Query {
notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED]
}
type Mutation {
markAsRead(id: ID!): NOTIFIED
}

View File

@ -1,9 +0,0 @@
type Notification {
id: ID!
read: Boolean
reason: ReasonNotification
createdAt: String
user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN")
comment: Comment @relation(name: "NOTIFIED", direction: "IN")
}

View File

@ -24,10 +24,12 @@ type User {
createdAt: String createdAt: String
updatedAt: String updatedAt: String
termsAndConditionsAgreedVersion: String termsAndConditionsAgreedVersion: String
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
@ -66,7 +68,7 @@ type User {
) )
comments: [Comment]! @relation(name: "WROTE", direction: "OUT") comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment)-[:COMMENTS]->(p:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true AND NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))") commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")

View File

@ -8,7 +8,6 @@ import createComment from './comments.js'
import createCategory from './categories.js' import createCategory from './categories.js'
import createTag from './tags.js' import createTag from './tags.js'
import createReport from './reports.js' import createReport from './reports.js'
import createNotification from './notifications.js'
export const seedServerHost = 'http://127.0.0.1:4001' export const seedServerHost = 'http://127.0.0.1:4001'
@ -31,7 +30,6 @@ const factories = {
Category: createCategory, Category: createCategory,
Tag: createTag, Tag: createTag,
Report: createReport, Report: createReport,
Notification: createNotification,
} }
export const cleanDatabase = async (options = {}) => { export const cleanDatabase = async (options = {}) => {

View File

@ -1,17 +0,0 @@
import uuid from 'uuid/v4'
export default function(params) {
const { id = uuid(), read = false } = params
return {
mutation: `
mutation($id: ID, $read: Boolean) {
CreateNotification(id: $id, read: $read) {
id
read
}
}
`,
variables: { id, read },
}
}

View File

@ -270,7 +270,7 @@ import Factory from './factories'
const hashtag1 = const hashtag1 =
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!' 'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
const hashtagAndMention1 = const hashtagAndMention1 =
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u3" href="/profile/u1">@peter-lustig</a> got that already. ;-)' 'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
await Promise.all([ await Promise.all([
asAdmin.create('Post', { asAdmin.create('Post', {

View File

@ -689,7 +689,7 @@
core-js "^2.6.5" core-js "^2.6.5"
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5": "@babel/runtime@^7.0.0", "@babel/runtime@^7.5.5":
version "7.5.5" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
@ -1546,6 +1546,14 @@ apollo-cache-control@0.8.2:
apollo-server-env "2.4.2" apollo-server-env "2.4.2"
graphql-extensions "0.10.1" graphql-extensions "0.10.1"
apollo-cache-control@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7"
integrity sha512-IZ1d3AXZtkZhLYo0kWqTbZ6nqLFaeUvLdMESs+9orMadBZ7mvzcAfBwrhKyCWPGeAAZ/jKv8FtYHybpchHgFAg==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.10.3"
apollo-cache-inmemory@~1.6.3: apollo-cache-inmemory@~1.6.3:
version "1.6.3" version "1.6.3"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d"
@ -1587,7 +1595,15 @@ apollo-datasource@0.6.2:
apollo-server-caching "0.5.0" apollo-server-caching "0.5.0"
apollo-server-env "2.4.2" apollo-server-env "2.4.2"
apollo-engine-reporting-protobuf@0.4.0: apollo-datasource@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13"
integrity sha512-gRYyFVpJgHE2hhS+VxMeOerxXQ/QYxWG7T6QddfugJWYAG9DRCl65e2b7txcGq2NP3r+O1iCm4GNwhRBDJbd8A==
dependencies:
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-engine-reporting-protobuf@0.4.0, apollo-engine-reporting-protobuf@^0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec" resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec"
integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA== integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==
@ -1607,6 +1623,19 @@ apollo-engine-reporting@1.4.4:
async-retry "^1.2.1" async-retry "^1.2.1"
graphql-extensions "0.10.1" graphql-extensions "0.10.1"
apollo-engine-reporting@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f"
integrity sha512-acfb7oFnru/8YQdY4x6+7WJbZfzdVETI8Cl+9ImgUrvUnE8P+f2SsGTKXTC1RuUvve4c56PAvaPgE+z8X1a1Mw==
dependencies:
apollo-engine-reporting-protobuf "^0.4.0"
apollo-graphql "^0.3.3"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-types "^0.2.4"
async-retry "^1.2.1"
graphql-extensions "^0.10.3"
apollo-env@0.5.1: apollo-env@0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3" resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3"
@ -1668,7 +1697,7 @@ apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
tslib "^1.9.3" tslib "^1.9.3"
zen-observable-ts "^0.8.19" zen-observable-ts "^0.8.19"
apollo-server-caching@0.5.0: apollo-server-caching@0.5.0, apollo-server-caching@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46"
integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw== integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==
@ -1702,6 +1731,33 @@ apollo-server-core@2.9.1:
subscriptions-transport-ws "^0.9.11" subscriptions-transport-ws "^0.9.11"
ws "^6.0.0" ws "^6.0.0"
apollo-server-core@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f"
integrity sha512-KQpOM3nAXdMqKVE0HHcOkH/EVhyDqFEKLNFlsyGHGOn9ujpI6RsltX+YpXRyAdbfQHpTk11v/IAo6XksWN+g1Q==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
"@apollographql/graphql-playground-html" "1.6.24"
"@types/graphql-upload" "^8.0.0"
"@types/ws" "^6.0.0"
apollo-cache-control "^0.8.4"
apollo-datasource "^0.6.3"
apollo-engine-reporting "^1.4.6"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-errors "^2.3.3"
apollo-server-plugin-base "^0.6.4"
apollo-server-types "^0.2.4"
apollo-tracing "^0.8.4"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "^0.10.3"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
graphql-upload "^8.0.2"
sha.js "^2.4.11"
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-env@2.4.2: apollo-server-env@2.4.2:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.2.tgz#8549caa7c8f57af88aadad5c2a0bb7adbcc5f76e" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.2.tgz#8549caa7c8f57af88aadad5c2a0bb7adbcc5f76e"
@ -1710,15 +1766,28 @@ apollo-server-env@2.4.2:
node-fetch "^2.1.2" node-fetch "^2.1.2"
util.promisify "^1.0.0" util.promisify "^1.0.0"
apollo-server-env@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-errors@2.3.2: apollo-server-errors@2.3.2:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.2.tgz#86bbd1ff8f0b5f16bfdcbb1760398928f9fce539" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.2.tgz#86bbd1ff8f0b5f16bfdcbb1760398928f9fce539"
integrity sha512-twVCP8tNHFzxOzU3jf84ppBFSvjvisZVWlgF82vwG+qEEUaAE5h5DVpeJbcI1vRW4VQPuFV+B+FIsnlweFKqtQ== integrity sha512-twVCP8tNHFzxOzU3jf84ppBFSvjvisZVWlgF82vwG+qEEUaAE5h5DVpeJbcI1vRW4VQPuFV+B+FIsnlweFKqtQ==
apollo-server-express@2.9.1, apollo-server-express@^2.9.0: apollo-server-errors@^2.3.3:
version "2.9.1" version "2.3.3"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.1.tgz#9a8cb7fba579e68ddfa1953dfd066b751bca32f0" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff"
integrity sha512-3mmuojt9s9Gyqdf8fbdKtbw23UFYrtVQtTNASgVW8zCabZqs2WjYnijMRf1aL4u9VSl+BFMOZUPMYaeBX+u38w== integrity sha512-MO4oJ129vuCcbqwr5ZwgxqGGiLz3hCyowz0bstUF7MR+vNGe4oe3DWajC9lv4CxrhcqUHQOeOPViOdIo1IxE3g==
apollo-server-express@^2.9.0, apollo-server-express@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.3.tgz#67573404030c2676be49a7bf97d423b8462e295c"
integrity sha512-Hkfs+ce6GqaoSzDOJs8Pj7W3YUjH0BzGglo5HMsOXOnjPZ0pJE9v8fmK76rlkITLw7GjvIq5GKlafymC31FMBw==
dependencies: dependencies:
"@apollographql/graphql-playground-html" "1.6.24" "@apollographql/graphql-playground-html" "1.6.24"
"@types/accepts" "^1.3.5" "@types/accepts" "^1.3.5"
@ -1726,10 +1795,11 @@ apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
"@types/cors" "^2.8.4" "@types/cors" "^2.8.4"
"@types/express" "4.17.1" "@types/express" "4.17.1"
accepts "^1.3.5" accepts "^1.3.5"
apollo-server-core "2.9.1" apollo-server-core "^2.9.3"
apollo-server-types "0.2.2" apollo-server-types "^0.2.4"
body-parser "^1.18.3" body-parser "^1.18.3"
cors "^2.8.4" cors "^2.8.4"
express "^4.17.1"
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0" graphql-tools "^4.0.0"
parseurl "^1.3.2" parseurl "^1.3.2"
@ -1743,6 +1813,13 @@ apollo-server-plugin-base@0.6.2:
dependencies: dependencies:
apollo-server-types "0.2.2" apollo-server-types "0.2.2"
apollo-server-plugin-base@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.4.tgz#63ea4fd0bbb6c4510bc8d0d2ad0a0684c8d0da8c"
integrity sha512-4rY+cBAIpQomGWYBtk8hHkLQWHrh5hgIBPQqmhXh00YFdcY+Ob1/cU2/2iqTcIzhtcaezsc8OZ63au6ahSBQqg==
dependencies:
apollo-server-types "^0.2.4"
apollo-server-testing@~2.9.1: apollo-server-testing@~2.9.1:
version "2.9.1" version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49" resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49"
@ -1759,13 +1836,22 @@ apollo-server-types@0.2.2:
apollo-server-caching "0.5.0" apollo-server-caching "0.5.0"
apollo-server-env "2.4.2" apollo-server-env "2.4.2"
apollo-server@~2.9.1: apollo-server-types@^0.2.4:
version "2.9.1" version "0.2.4"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.1.tgz#16ff443d43ea38f72fe20adea0803c46037b2b3b" resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.4.tgz#28864900ffc7f9711a859297c143a833fdb6aa43"
integrity sha512-iCGoRBOvwTUkDz6Nq/rKguMyhDiQdL3VneF0GTjBGrelTIp3YTIxk/qBFkIr2Chtm9ZZYkS6o+ZldUnxYFKg7A== integrity sha512-G4FvBVgGQcTW6ZBS2+hvcDQkSfdOIKV+cHADduXA275v+5zl42g+bCaGd/hCCKTDRjmQvObLiMxH/BJ6pDMQgA==
dependencies: dependencies:
apollo-server-core "2.9.1" apollo-engine-reporting-protobuf "^0.4.0"
apollo-server-express "2.9.1" apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server@~2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.3.tgz#2a79fcee25da0b0673eb70d73839c40c3c4b8cca"
integrity sha512-JQoeseSo3yOBu3WJzju0NTreoqYckNILybgXNUOhdurE55VFpZ8dsBEO6nMfdO2y1A70W14mnnVWCBEm+1rE8w==
dependencies:
apollo-server-core "^2.9.3"
apollo-server-express "^2.9.3"
express "^4.0.0" express "^4.0.0"
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0" graphql-tools "^4.0.0"
@ -1778,6 +1864,14 @@ apollo-tracing@0.8.2:
apollo-server-env "2.4.2" apollo-server-env "2.4.2"
graphql-extensions "0.10.1" graphql-extensions "0.10.1"
apollo-tracing@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de"
integrity sha512-DjbFW0IvHicSlTVG+vK+1WINfBMRCdPPHJSW/j65JMir9Oe56WGeqL8qz8hptdUUmLYEb+azvcyyGsJsiR3zpQ==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.10.3"
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
@ -3311,12 +3405,12 @@ eslint-module-utils@^2.4.0:
debug "^2.6.8" debug "^2.6.8"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
eslint-plugin-es@^1.4.0: eslint-plugin-es@^1.4.1:
version "1.4.0" version "1.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6" resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw== integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
dependencies: dependencies:
eslint-utils "^1.3.0" eslint-utils "^1.4.2"
regexpp "^2.0.1" regexpp "^2.0.1"
eslint-plugin-import@~2.18.2: eslint-plugin-import@~2.18.2:
@ -3336,20 +3430,20 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
resolve "^1.11.0" resolve "^1.11.0"
eslint-plugin-jest@~22.15.2: eslint-plugin-jest@~22.16.0:
version "22.15.2" version "22.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg== integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
dependencies: dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0" "@typescript-eslint/experimental-utils" "^1.13.0"
eslint-plugin-node@~9.1.0: eslint-plugin-node@~9.2.0:
version "9.1.0" version "9.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a" resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw== integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
dependencies: dependencies:
eslint-plugin-es "^1.4.0" eslint-plugin-es "^1.4.1"
eslint-utils "^1.3.1" eslint-utils "^1.4.2"
ignore "^5.1.1" ignore "^5.1.1"
minimatch "^3.0.4" minimatch "^3.0.4"
resolve "^1.10.1" resolve "^1.10.1"
@ -3388,7 +3482,7 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-utils@^1.3.0, eslint-utils@^1.3.1, eslint-utils@^1.4.2: eslint-utils@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
@ -3400,10 +3494,10 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@~6.2.2: eslint@~6.3.0:
version "6.2.2" version "6.3.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f" resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a"
integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw== integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==
dependencies: dependencies:
"@babel/code-frame" "^7.0.0" "@babel/code-frame" "^7.0.0"
ajv "^6.10.0" ajv "^6.10.0"
@ -4086,6 +4180,15 @@ graphql-extensions@0.10.1:
apollo-server-env "2.4.2" apollo-server-env "2.4.2"
apollo-server-types "0.2.2" apollo-server-types "0.2.2"
graphql-extensions@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea"
integrity sha512-kwU0gUe+Qdfr8iZYT91qrPSwQNgPhB/ClF1m1LEPdxlptk5FhFmjpxAcbMZ8q7j0kjfnbp2IeV1OhRDCEPqz2w==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
apollo-server-env "^2.4.3"
apollo-server-types "^0.2.4"
graphql-import@0.7.1: graphql-import@0.7.1:
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223" resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223"
@ -4180,10 +4283,10 @@ graphql-upload@^8.0.2:
http-errors "^1.7.2" http-errors "^1.7.2"
object-path "^0.11.4" object-path "^0.11.4"
graphql@^14.2.1, graphql@^14.5.3: graphql@^14.2.1, graphql@^14.5.4:
version "14.5.3" version "14.5.4"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg== integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
dependencies: dependencies:
iterall "^1.2.2" iterall "^1.2.2"
@ -6175,12 +6278,12 @@ neo-async@^2.6.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.5: neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
version "1.7.5" version "1.7.6"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4" resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw== integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
dependencies: dependencies:
"@babel/runtime" "^7.4.4" "@babel/runtime" "^7.5.5"
text-encoding-utf-8 "^1.0.2" text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"

View File

@ -28,6 +28,17 @@ To start the services that are required for cypress testing, run this:
$ yarn cypress:setup $ yarn cypress:setup
``` ```
## Install cypress
Even if the required services for testing run via docker, depending on your
setup, the cypress tests themselves run on your host machine. So with our
without docker, you would have to install cypress and its dependencies first:
```
# in the root folder /
yarn install
```
## Run cypress ## Run cypress
After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command: After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command:

View File

@ -1,5 +1,5 @@
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps"; import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
import { getLangByName } from "../../support/helpers"; import helpers from "../../support/helpers";
import slugify from "slug"; import slugify from "slug";
// import { VERSION } from '../../../webapp/pages/terms-and-conditions.vue'; // import { VERSION } from '../../../webapp/pages/terms-and-conditions.vue';
@ -140,14 +140,17 @@ Then("I am still logged in", () => {
When("I select {string} in the language menu", name => { When("I select {string} in the language menu", name => {
cy.switchLanguage(name, true); cy.switchLanguage(name, true);
}); });
Given("I previously switched the language to {string}", name => { Given("I previously switched the language to {string}", name => {
cy.switchLanguage(name, true); cy.switchLanguage(name, true);
}); });
Then("the whole user interface appears in {string}", name => { Then("the whole user interface appears in {string}", name => {
const lang = getLangByName(name); const { code } = helpers.getLangByName(name);
cy.get(`html[lang=${lang.code}]`); cy.get(`html[lang=${code}]`);
cy.getCookie("locale").should("have.property", "value", lang.code); cy.getCookie("locale").should("have.property", "value", code);
}); });
Then("I see a button with the label {string}", label => { Then("I see a button with the label {string}", label => {
cy.contains("button", label); cy.contains("button", label);
}); });
@ -175,13 +178,13 @@ Given("we have the following posts in our database:", table => {
}; };
postAttributes.deleted = Boolean(postAttributes.deleted); postAttributes.deleted = Boolean(postAttributes.deleted);
const disabled = Boolean(postAttributes.disabled); const disabled = Boolean(postAttributes.disabled);
postAttributes.categoryIds = [`cat${i}`]; postAttributes.categoryIds = [`cat${i}${new Date()}`];
postAttributes; postAttributes;
cy.factory() cy.factory()
.create("User", userAttributes) .create("User", userAttributes)
.authenticateAs(userAttributes) .authenticateAs(userAttributes)
.create("Category", { .create("Category", {
id: `cat${i}`, id: `cat${i}${new Date()}`,
name: "Just For Fun", name: "Just For Fun",
slug: `just-for-fun-${i}`, slug: `just-for-fun-${i}`,
icon: "smile" icon: "smile"
@ -364,7 +367,7 @@ When("mention {string} in the text", mention => {
}); });
Then("the notification gets marked as read", () => { Then("the notification gets marked as read", () => {
cy.get(".post.createdAt") cy.get(".notifications-menu-popover .notification")
.first() .first()
.should("have.class", "read"); .should("have.class", "read");
}); });

View File

@ -26,6 +26,9 @@ Feature: Block a User
And nobody is following the user profile anymore And nobody is following the user profile anymore
Scenario: Posts of blocked users are filtered from search results Scenario: Posts of blocked users are filtered from search results
Given we have the following posts in our database:
| Author | id | title | content |
| Some unblocked user | im-not-blocked | Post that should be seen | cause I'm not blocked |
Given "Spammy Spammer" wrote a post "Spam Spam Spam" Given "Spammy Spammer" wrote a post "Spam Spam Spam"
When I search for "Spam" When I search for "Spam"
Then I should see the following posts in the select dropdown: Then I should see the following posts in the select dropdown:
@ -35,3 +38,7 @@ Feature: Block a User
And I refresh the page And I refresh the page
And I search for "Spam" And I search for "Spam"
Then the search has no results Then the search has no results
But I search for "not blocked"
Then I should see the following posts in the select dropdown:
| title |
| Post that should be seen |

View File

@ -14,7 +14,7 @@
/* globals Cypress cy */ /* globals Cypress cy */
import "cypress-file-upload"; import "cypress-file-upload";
import { getLangByName } from "./helpers"; import helpers from "./helpers";
import users from "../fixtures/users.json"; import users from "../fixtures/users.json";
const switchLang = name => { const switchLang = name => {
@ -22,8 +22,9 @@ const switchLang = name => {
cy.contains(".locale-menu-popover a", name).click(); cy.contains(".locale-menu-popover a", name).click();
}; };
Cypress.Commands.add("switchLanguage", (name, force) => { Cypress.Commands.add("switchLanguage", (name, force) => {
const code = getLangByName(name).code; const { code } = helpers.getLangByName(name);
if (force) { if (force) {
switchLang(name); switchLang(name);
} else { } else {

View File

@ -1,10 +1,8 @@
import find from 'lodash/find' import find from 'lodash/find'
import locales from '../../webapp/locales'
const helpers = { export default {
locales: require('../../webapp/locales'), getLangByName(name) {
getLangByName: name => { return find(locales, { name })
return find(helpers.locales, { name })
} }
} }
export default helpers

View File

@ -30,17 +30,6 @@
memory: "1G" memory: "1G"
limits: limits:
memory: "2G" memory: "2G"
env:
- name: NEO4J_apoc_import_file_enabled
value: "true"
- name: NEO4J_dbms_memory_pagecache_size
value: "490M"
- name: NEO4J_dbms_memory_heap_max__size
value: "500M"
- name: NEO4J_dbms_memory_heap_initial__size
value: "500M"
- name: NEO4J_dbms_security_procedures_unrestricted
value: "algo.*,apoc.*"
envFrom: envFrom:
- configMapRef: - configMapRef:
name: configmap name: configmap

View File

@ -9,6 +9,11 @@
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687" NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
NEO4J_AUTH: "none" NEO4J_AUTH: "none"
CLIENT_URI: "https://nitro-staging.human-connection.org" CLIENT_URI: "https://nitro-staging.human-connection.org"
NEO4J_apoc_import_file_enabled: "true"
NEO4J_dbms_memory_pagecache_size: "490M"
NEO4J_dbms_memory_heap_max__size: "500M"
NEO4J_dbms_memory_heap_initial__size: "500M"
NEO4J_dbms_security_procedures_unrestricted: "algo.*,apoc.*"
SENTRY_DSN_WEBAPP: "" SENTRY_DSN_WEBAPP: ""
SENTRY_DSN_BACKEND: "" SENTRY_DSN_BACKEND: ""
COMMIT: "" COMMIT: ""

View File

@ -137,8 +137,8 @@ p.contentExcerpt = post.contentExcerpt,
p.visibility = toLower(post.visibility), p.visibility = toLower(post.visibility),
p.createdAt = post.createdAt.`$date`, p.createdAt = post.createdAt.`$date`,
p.updatedAt = post.updatedAt.`$date`, p.updatedAt = post.updatedAt.`$date`,
p.deleted = COALESCE(post.deleted,false), p.deleted = COALESCE(post.deleted, false),
p.disabled = NOT post.isEnabled p.disabled = COALESCE(NOT post.isEnabled, false)
WITH p, post WITH p, post
MATCH (u:User {id: post.userId}) MATCH (u:User {id: post.userId})
MERGE (u)-[:WROTE]->(p) MERGE (u)-[:WROTE]->(p)

View File

@ -23,13 +23,13 @@
"codecov": "^3.5.0", "codecov": "^3.5.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"cypress": "^3.4.1", "cypress": "^3.4.1",
"cypress-cucumber-preprocessor": "^1.15.1", "cypress-cucumber-preprocessor": "^1.16.0",
"cypress-file-upload": "^3.3.3", "cypress-file-upload": "^3.3.3",
"cypress-plugin-retries": "^1.2.2", "cypress-plugin-retries": "^1.2.2",
"dotenv": "^8.1.0", "dotenv": "^8.1.0",
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2", "graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.5", "neo4j-driver": "^1.7.6",
"neode": "^0.3.2", "neode": "^0.3.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"slug": "^1.1.0" "slug": "^1.1.0"

View File

@ -3,6 +3,7 @@ import Editor from './Editor'
import Vuex from 'vuex' import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
import MutationObserver from 'mutation-observer' import MutationObserver from 'mutation-observer'
import Vue from 'vue'
global.MutationObserver = MutationObserver global.MutationObserver = MutationObserver
@ -55,9 +56,11 @@ describe('Editor.vue', () => {
propsData.value = 'I am a piece of text' propsData.value = 'I am a piece of text'
}) })
it.skip('renders', () => { it('renders', async () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('.ProseMirror').text()).toContain('I am a piece of text') await Vue.nextTick().then(() => {
expect(wrapper.find('.editor-content').text()).toContain(propsData.value)
})
}) })
}) })
@ -88,6 +91,29 @@ describe('Editor.vue', () => {
) )
}) })
describe('limists suggestion list to 15 users', () => {
beforeEach(() => {
let manyUsersList = []
for (let i = 0; i < 25; i++) {
manyUsersList.push({ id: `user${i}` })
}
propsData.users = manyUsersList
wrapper = Wrapper()
})
it('when query is empty', () => {
expect(
wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users),
).toHaveLength(15)
})
it('when query is present', () => {
expect(
wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users, 'user'),
).toHaveLength(15)
})
})
it('sets the Hashtag items to the hashtags', () => { it('sets the Hashtag items to the hashtags', () => {
propsData.hashtags = [ propsData.hashtags = [
{ {
@ -105,6 +131,29 @@ describe('Editor.vue', () => {
}), }),
) )
}) })
describe('limists suggestion list to 15 hashtags', () => {
beforeEach(() => {
let manyHashtagsList = []
for (let i = 0; i < 25; i++) {
manyHashtagsList.push({ id: `hashtag${i}` })
}
propsData.hashtags = manyHashtagsList
wrapper = Wrapper()
})
it('when query is empty', () => {
expect(
wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags),
).toHaveLength(15)
})
it('when query is present', () => {
expect(
wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags, 'hashtag'),
).toHaveLength(15)
})
})
}) })
}) })
}) })

View File

@ -203,12 +203,14 @@ export default {
filterSuggestionList(items, query) { filterSuggestionList(items, query) {
query = this.sanitizeQuery(query) query = this.sanitizeQuery(query)
if (!query) { if (!query) {
return items return items.slice(0, 15)
} }
return items.filter(item => {
const filteredList = items.filter(item => {
const itemString = item.slug || item.id const itemString = item.slug || item.id
return itemString.toLowerCase().includes(query.toLowerCase()) return itemString.toLowerCase().includes(query.toLowerCase())
}) })
return filteredList.slice(0, 15)
}, },
sanitizeQuery(query) { sanitizeQuery(query) {
if (this.suggestionType === HASHTAG) { if (this.suggestionType === HASHTAG) {

View File

@ -37,9 +37,9 @@ describe('Notification', () => {
describe('given a notification about a comment on a post', () => { describe('given a notification about a comment on a post', () => {
beforeEach(() => { beforeEach(() => {
propsData.notification = { propsData.notification = {
reason: 'comment_on_post', reason: 'commented_on_post',
post: null, from: {
comment: { __typename: 'Comment',
id: 'comment-1', id: 'comment-1',
contentExcerpt: contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.', '<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
@ -56,7 +56,7 @@ describe('Notification', () => {
it('renders reason', () => { it('renders reason', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual( expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.comment_on_post', 'notifications.menu.commented_on_post',
) )
}) })
it('renders title', () => { it('renders title', () => {
@ -92,14 +92,14 @@ describe('Notification', () => {
beforeEach(() => { beforeEach(() => {
propsData.notification = { propsData.notification = {
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
post: { from: {
__typename: 'Post',
title: "It's a post title", title: "It's a post title",
id: 'post-1', id: 'post-1',
slug: 'its-a-title', slug: 'its-a-title',
contentExcerpt: contentExcerpt:
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.', '<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
}, },
comment: null,
} }
}) })
@ -138,8 +138,8 @@ describe('Notification', () => {
beforeEach(() => { beforeEach(() => {
propsData.notification = { propsData.notification = {
reason: 'mentioned_in_comment', reason: 'mentioned_in_comment',
post: null, from: {
comment: { __typename: 'Comment',
id: 'comment-1', id: 'comment-1',
contentExcerpt: contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.', '<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',

View File

@ -1,14 +1,8 @@
<template> <template>
<ds-space :class="[{ read: notification.read }, notification]" margin-bottom="x-small"> <ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
<client-only> <client-only>
<ds-space margin-bottom="x-small"> <ds-space margin-bottom="x-small">
<hc-user <hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
v-if="resourceType == 'Post'"
:user="post.author"
:date-time="post.createdAt"
:trunc="35"
/>
<hc-user v-else :user="comment.author" :date-time="comment.createdAt" :trunc="35" />
</ds-space> </ds-space>
<ds-text class="reason-text-for-test" color="soft"> <ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.menu.${notification.reason}`) }} {{ $t(`notifications.menu.${notification.reason}`) }}
@ -22,16 +16,15 @@
> >
<ds-space margin-bottom="x-small"> <ds-space margin-bottom="x-small">
<ds-card <ds-card
:header="post.title || comment.post.title" :header="from.title || from.post.title"
hover hover
space="x-small" space="x-small"
class="notifications-card" class="notifications-card"
> >
<ds-space margin-bottom="x-small" /> <ds-space margin-bottom="x-small" />
<div v-if="resourceType == 'Post'">{{ post.contentExcerpt | removeHtml }}</div> <div>
<div v-else> <span v-if="isComment" class="comment-notification-header">Comment:</span>
<span class="comment-notification-header">Comment:</span> {{ from.contentExcerpt | removeHtml }}
{{ comment.contentExcerpt | removeHtml }}
</div> </div>
</ds-card> </ds-card>
</ds-space> </ds-space>
@ -54,23 +47,21 @@ export default {
}, },
}, },
computed: { computed: {
resourceType() { from() {
return this.post.id ? 'Post' : 'Comment' return this.notification.from
}, },
post() { isComment() {
return this.notification.post || {} return this.from.__typename === 'Comment'
},
comment() {
return this.notification.comment || {}
}, },
params() { params() {
const post = this.isComment ? this.from.post : this.from
return { return {
id: this.post.id || this.comment.post.id, id: post.id,
slug: this.post.slug || this.comment.post.slug, slug: post.slug,
} }
}, },
hashParam() { hashParam() {
return this.post.id ? {} : { hash: `#commentId-${this.comment.id}` } return this.isComment ? { hash: `#commentId-${this.from.id}` } : {}
}, },
}, },
} }

View File

@ -40,9 +40,9 @@ describe('NotificationList.vue', () => {
propsData = { propsData = {
notifications: [ notifications: [
{ {
id: 'notification-41',
read: false, read: false,
post: { from: {
__typename: 'Post',
id: 'post-1', id: 'post-1',
title: 'some post title', title: 'some post title',
slug: 'some-post-title', slug: 'some-post-title',
@ -55,9 +55,9 @@ describe('NotificationList.vue', () => {
}, },
}, },
{ {
id: 'notification-42',
read: false, read: false,
post: { from: {
__typename: 'Post',
id: 'post-2', id: 'post-2',
title: 'another post title', title: 'another post title',
slug: 'another-post-title', slug: 'another-post-title',
@ -115,9 +115,9 @@ describe('NotificationList.vue', () => {
.trigger('click') .trigger('click')
}) })
it("emits 'markAsRead' with the notificationId", () => { it("emits 'markAsRead' with the id of the notification source", () => {
expect(wrapper.emitted('markAsRead')).toBeTruthy() expect(wrapper.emitted('markAsRead')).toBeTruthy()
expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42']) expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-2'])
}) })
}) })
}) })

View File

@ -4,7 +4,7 @@
v-for="notification in notifications" v-for="notification in notifications"
:key="notification.id" :key="notification.id"
:notification="notification" :notification="notification"
@read="markAsRead(notification.id)" @read="markAsRead(notification.from.id)"
/> />
</div> </div>
</template> </template>
@ -24,8 +24,8 @@ export default {
}, },
}, },
methods: { methods: {
markAsRead(notificationId) { markAsRead(notificationSourceId) {
this.$emit('markAsRead', notificationId) this.$emit('markAsRead', notificationSourceId)
}, },
}, },
} }

View File

@ -18,7 +18,7 @@
<script> <script>
import Dropdown from '~/components/Dropdown' import Dropdown from '~/components/Dropdown'
import { currentUserNotificationsQuery, updateNotificationMutation } from '~/graphql/User' import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import NotificationList from '../NotificationList/NotificationList' import NotificationList from '../NotificationList/NotificationList'
export default { export default {
@ -27,36 +27,41 @@ export default {
NotificationList, NotificationList,
Dropdown, Dropdown,
}, },
data() {
return {
notifications: [],
}
},
props: { props: {
placement: { type: String }, placement: { type: String },
}, },
computed: {
totalNotifications() {
return (this.notifications || []).length
},
},
methods: { methods: {
async markAsRead(notificationId) { async markAsRead(notificationSourceId) {
const variables = { id: notificationId, read: true } const variables = { id: notificationSourceId }
try { try {
await this.$apollo.mutate({ const {
mutation: updateNotificationMutation(), data: { markAsRead },
} = await this.$apollo.mutate({
mutation: markAsReadMutation(),
variables, variables,
}) })
if (!(markAsRead && markAsRead.read === true)) return
this.notifications = this.notifications.map(n => {
return n.from.id === markAsRead.from.id ? markAsRead : n
})
} catch (err) { } catch (err) {
throw new Error(err) throw new Error(err)
} }
}, },
}, },
computed: {
totalNotifications() {
return this.notifications.length
},
},
apollo: { apollo: {
notifications: { notifications: {
query: currentUserNotificationsQuery(), query: notificationQuery(),
update: data => {
const {
currentUser: { notifications },
} = data
return notifications
},
}, },
}, },
} }

View File

@ -1,5 +1,58 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
const fragments = gql`
fragment post on Post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
fragment comment on Comment {
id
createdAt
disabled
deleted
contentExcerpt
author {
id
slug
name
disabled
deleted
avatar
}
post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
}
`
export default i18n => { export default i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
@ -76,77 +129,37 @@ export default i18n => {
` `
} }
export const currentUserNotificationsQuery = () => { export const notificationQuery = () => {
return gql` return gql`
${fragments}
query { query {
currentUser { notifications(read: false, orderBy: createdAt_desc) {
id read
notifications(read: false, orderBy: createdAt_desc) { reason
id createdAt
read from {
reason __typename
createdAt ...post
post { ...comment
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
comment {
id
createdAt
disabled
deleted
contentExcerpt
author {
id
slug
name
disabled
deleted
avatar
}
post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
}
} }
} }
} }
` `
} }
export const updateNotificationMutation = () => { export const markAsReadMutation = () => {
return gql` return gql`
mutation($id: ID!, $read: Boolean!) { ${fragments}
UpdateNotification(id: $id, read: $read) { mutation($id: ID!) {
id markAsRead(id: $id) {
read read
reason
createdAt
from {
__typename
...post
...comment
}
} }
} }
` `

View File

@ -134,7 +134,7 @@
"menu": { "menu": {
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …", "mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …", "mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
"comment_on_post": "Hat deinen Beitrag kommentiert …" "commented_on_post": "Hat deinen Beitrag kommentiert …"
} }
}, },
"search": { "search": {

View File

@ -134,7 +134,7 @@
"menu": { "menu": {
"mentioned_in_post": "Mentioned you in a post …", "mentioned_in_post": "Mentioned you in a post …",
"mentioned_in_comment": "Mentioned you in a comment …", "mentioned_in_comment": "Mentioned you in a comment …",
"comment_on_post": "Commented on your post …" "commented_on_post": "Commented on your post …"
} }
}, },
"search": { "search": {

View File

@ -63,7 +63,7 @@
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.1", "date-fns": "2.0.1",
"express": "~4.17.1", "express": "~4.17.1",
"graphql": "~14.5.3", "graphql": "~14.5.4",
"isemail": "^3.2.0", "isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0", "linkify-it": "~2.2.0",
@ -78,7 +78,7 @@
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
"vue-infinite-scroll": "^2.0.2", "vue-infinite-scroll": "^2.0.2",
"vue-izitoast": "^1.2.0", "vue-izitoast": "^1.2.1",
"vue-sweetalert-icons": "~4.2.0", "vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1", "vuex-i18n": "~1.13.1",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
@ -106,8 +106,8 @@
"eslint-config-standard": "~12.0.0", "eslint-config-standard": "~12.0.0",
"eslint-loader": "~3.0.0", "eslint-loader": "~3.0.0",
"eslint-plugin-import": "~2.18.2", "eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.2", "eslint-plugin-jest": "~22.16.0",
"eslint-plugin-node": "~9.1.0", "eslint-plugin-node": "~9.2.0",
"eslint-plugin-prettier": "~3.1.0", "eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1", "eslint-plugin-standard": "~4.0.1",
@ -119,7 +119,7 @@
"node-sass": "~4.12.0", "node-sass": "~4.12.0",
"nodemon": "~1.19.1", "nodemon": "~1.19.1",
"prettier": "~1.18.2", "prettier": "~1.18.2",
"sass-loader": "~7.3.1", "sass-loader": "~8.0.0",
"style-loader": "~0.23.1", "style-loader": "~0.23.1",
"style-resources-loader": "~1.2.1", "style-resources-loader": "~1.2.1",
"tippy.js": "^4.3.5", "tippy.js": "^4.3.5",

View File

@ -185,9 +185,6 @@ export default {
return result return result
}, },
update({ Post }) { update({ Post }) {
// TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same
// result set twice.
this.hasMore = Post.length >= this.pageSize this.hasMore = Post.length >= this.pageSize
const posts = uniqBy([...this.posts, ...Post], 'id') const posts = uniqBy([...this.posts, ...Post], 'id')
this.posts = posts this.posts = posts

View File

@ -104,9 +104,7 @@ describe('ProfileSlug', () => {
describe('currently no posts available (e.g. after tab switching)', () => { describe('currently no posts available (e.g. after tab switching)', () => {
beforeEach(() => { beforeEach(() => {
wrapper.setData({ wrapper.setData({ posts: [], hasMore: false })
Post: null,
})
}) })
it('displays no "load more" button', () => { it('displays no "load more" button', () => {
@ -137,9 +135,7 @@ describe('ProfileSlug', () => {
} }
}) })
wrapper.setData({ wrapper.setData({ posts, hasMore: true })
Post: posts,
})
}) })
it('displays a "load more" button', () => { it('displays a "load more" button', () => {
@ -170,9 +166,7 @@ describe('ProfileSlug', () => {
} }
}) })
wrapper.setData({ wrapper.setData({ posts, hasMore: false })
Post: posts,
})
}) })
it('displays no "load more" button', () => { it('displays no "load more" button', () => {

View File

@ -219,8 +219,8 @@
</ds-space> </ds-space>
</ds-grid-item> </ds-grid-item>
<template v-if="activePosts.length"> <template v-if="posts.length">
<masonry-grid-item v-for="(post, index) in activePosts" :key="post.id"> <masonry-grid-item v-for="(post, index) in posts" :key="post.id">
<hc-post-card <hc-post-card
:post="post" :post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }" :width="{ base: '100%', md: '100%', xl: '50%' }"
@ -229,10 +229,10 @@
</masonry-grid-item> </masonry-grid-item>
</template> </template>
<template v-else-if="$apollo.loading"> <template v-else-if="$apollo.loading">
<ds-grid-item> <ds-grid-item column-span="fullWidth">
<ds-section centered> <ds-space centered>
<ds-spinner size="base"></ds-spinner> <ds-spinner size="base"></ds-spinner>
</ds-section> </ds-space>
</ds-grid-item> </ds-grid-item>
</template> </template>
<template v-else> <template v-else>
@ -306,33 +306,21 @@ export default {
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id }) const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
return { return {
User: [], User: [],
Post: [], posts: [],
activePosts: [], hasMore: false,
voted: false, offset: 0,
page: 1,
pageSize: 6, pageSize: 6,
tabActive: 'post', tabActive: 'post',
filter, filter,
} }
}, },
computed: { computed: {
hasMore() {
const total = {
post: this.user.contributionsCount,
shout: this.user.shoutedCount,
comment: this.user.commentedCount,
}[this.tabActive]
return this.Post && this.Post.length < total
},
myProfile() { myProfile() {
return this.$route.params.id === this.$store.getters['auth/user'].id return this.$route.params.id === this.$store.getters['auth/user'].id
}, },
user() { user() {
return this.User ? this.User[0] : {} return this.User ? this.User[0] : {}
}, },
offset() {
return (this.page - 1) * this.pageSize
},
socialMediaLinks() { socialMediaLinks() {
const { socialMedia = [] } = this.user const { socialMedia = [] } = this.user
return socialMedia.map(socialMedia => { return socialMedia.map(socialMedia => {
@ -355,19 +343,15 @@ export default {
throw new Error('User not found!') throw new Error('User not found!')
} }
}, },
Post(val) {
this.activePosts = this.setActivePosts()
},
}, },
methods: { methods: {
removePostFromList(index) { removePostFromList(index) {
this.activePosts.splice(index, 1) this.posts.splice(index, 1)
this.$apollo.queries.User.refetch()
}, },
handleTab(tab) { handleTab(tab) {
this.tabActive = tab this.tabActive = tab
this.Post = null
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id }) this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
this.resetPostList()
}, },
uniq(items, field = 'id') { uniq(items, field = 'id') {
return uniqBy(items, field) return uniqBy(items, field)
@ -377,38 +361,23 @@ export default {
this.$apollo.queries.User.refetch() this.$apollo.queries.User.refetch()
}, },
showMoreContributions() { showMoreContributions() {
// this.page++ this.offset += this.pageSize
// Fetch more data and transform the original result
this.page++
this.$apollo.queries.Post.fetchMore({
variables: {
filter: this.filter,
first: this.pageSize,
offset: this.offset,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
let output = { Post: this.Post }
output.Post = [...previousResult.Post, ...fetchMoreResult.Post]
return output
},
fetchPolicy: 'cache-and-network',
})
}, },
setActivePosts() { resetPostList() {
if (!this.Post) { this.offset = 0
return [] this.posts = []
} this.hasMore = false
return this.uniq(this.Post.filter(post => !post.deleted))
}, },
async block(user) { async block(user) {
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } }) await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
this.$apollo.queries.User.refetch() this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch() this.$apollo.queries.Post.refetch()
}, },
async unblock(user) { async unblock(user) {
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } }) await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
this.$apollo.queries.User.refetch() this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch() this.$apollo.queries.Post.refetch()
}, },
}, },
@ -421,10 +390,19 @@ export default {
return { return {
filter: this.filter, filter: this.filter,
first: this.pageSize, first: this.pageSize,
offset: 0, offset: this.offset,
orderBy: 'createdAt_desc',
} }
}, },
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
update({ Post }) {
if (!Post) return
// TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same
// result set twice.
this.hasMore = Post.length >= this.pageSize
this.posts = this.uniq([...this.posts, ...Post])
},
}, },
User: { User: {
query() { query() {

View File

@ -1,3 +1,10 @@
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
import introspectionQueryResultData from './apollo-config/fragmentTypes.json'
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
})
export default ({ app }) => { export default ({ app }) => {
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000' const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
@ -10,5 +17,6 @@ export default ({ app }) => {
tokenName: 'human-connection-token', tokenName: 'human-connection-token',
persisting: false, persisting: false,
websocketsOnly: false, websocketsOnly: false,
cache: new InMemoryCache({ fragmentMatcher }),
} }
} }

View File

@ -0,0 +1,18 @@
{
"__schema": {
"types": [
{
"kind": "UNION",
"name": "NotificationSource",
"possibleTypes": [
{
"name": "Post"
},
{
"name": "Comment"
}
]
}
]
}
}

View File

@ -91,23 +91,6 @@ export const actions = {
id id
url url
} }
notifications(read: false, orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
} }
} }
`, `,

View File

@ -1,99 +0,0 @@
import gql from 'graphql-tag'
export const state = () => {
return {
notifications: null,
pending: false,
}
}
export const mutations = {
SET_NOTIFICATIONS(state, notifications) {
state.notifications = notifications
},
SET_PENDING(state, pending) {
state.pending = pending
},
UPDATE_NOTIFICATIONS(state, notification) {
const notifications = state.notifications
const toBeUpdated = notifications.find(n => {
return n.id === notification.id
})
state.notifications = {
...toBeUpdated,
...notification,
}
},
}
export const getters = {
notifications(state) {
return !!state.notifications
},
}
export const actions = {
async init({ getters, commit }) {
if (getters.notifications) return
commit('SET_PENDING', true)
const client = this.app.apolloProvider.defaultClient
let notifications
try {
const {
data: { currentUser },
} = await client.query({
query: gql`
{
currentUser {
id
notifications(orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
}
}
`,
})
notifications = currentUser.notifications
commit('SET_NOTIFICATIONS', notifications)
} finally {
commit('SET_PENDING', false)
}
return notifications
},
async markAsRead({ commit, rootGetters }, notificationId) {
const client = this.app.apolloProvider.defaultClient
const mutation = gql`
mutation($id: ID!, $read: Boolean!) {
UpdateNotification(id: $id, read: $read) {
id
read
}
}
`
const variables = {
id: notificationId,
read: true,
}
const {
data: { UpdateNotification },
} = await client.mutate({
mutation,
variables,
})
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
},
}

View File

@ -46,8 +46,8 @@ export const actions = {
await this.app.apolloProvider.defaultClient await this.app.apolloProvider.defaultClient
.query({ .query({
query: gql` query: gql`
query findPosts($query: String!) { query findPosts($query: String!, $filter: _PostFilter) {
findPosts(query: $query, limit: 10) { findPosts(query: $query, limit: 10, filter: $filter) {
id id
slug slug
label: title label: title
@ -64,6 +64,7 @@ export const actions = {
`, `,
variables: { variables: {
query: value.replace(/\s/g, '~ ') + '~', query: value.replace(/\s/g, '~ ') + '~',
filter: {},
}, },
}) })
.then(res => { .then(res => {

View File

@ -6379,12 +6379,12 @@ eslint-module-utils@^2.4.0:
debug "^2.6.8" debug "^2.6.8"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
eslint-plugin-es@^1.4.0: eslint-plugin-es@^1.4.1:
version "1.4.0" version "1.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6" resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw== integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
dependencies: dependencies:
eslint-utils "^1.3.0" eslint-utils "^1.4.2"
regexpp "^2.0.1" regexpp "^2.0.1"
eslint-plugin-import@~2.18.2: eslint-plugin-import@~2.18.2:
@ -6404,20 +6404,20 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
resolve "^1.11.0" resolve "^1.11.0"
eslint-plugin-jest@~22.15.2: eslint-plugin-jest@~22.16.0:
version "22.15.2" version "22.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg== integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
dependencies: dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0" "@typescript-eslint/experimental-utils" "^1.13.0"
eslint-plugin-node@~9.1.0: eslint-plugin-node@~9.2.0:
version "9.1.0" version "9.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a" resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw== integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
dependencies: dependencies:
eslint-plugin-es "^1.4.0" eslint-plugin-es "^1.4.1"
eslint-utils "^1.3.1" eslint-utils "^1.4.2"
ignore "^5.1.1" ignore "^5.1.1"
minimatch "^3.0.4" minimatch "^3.0.4"
resolve "^1.10.1" resolve "^1.10.1"
@ -6455,7 +6455,7 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-utils@^1.3.0, eslint-utils@^1.3.1: eslint-utils@^1.3.1, eslint-utils@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
@ -7627,10 +7627,10 @@ graphql-upload@^8.0.2:
http-errors "^1.7.2" http-errors "^1.7.2"
object-path "^0.11.4" object-path "^0.11.4"
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.3: "graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.4:
version "14.5.3" version "14.5.4"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg== integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
dependencies: dependencies:
iterall "^1.2.2" iterall "^1.2.2"
@ -8154,12 +8154,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.1: ignore@^5.1.1, ignore@^5.1.4:
version "5.1.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5"
integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==
ignore@^5.1.4:
version "5.1.4" version "5.1.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
@ -13337,7 +13332,7 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3" scss-tokenizer "^0.2.3"
yargs "^7.0.0" yargs "^7.0.0"
sass-loader@^7.1.0, sass-loader@~7.3.1: sass-loader@^7.1.0:
version "7.3.1" version "7.3.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA== integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
@ -13348,6 +13343,17 @@ sass-loader@^7.1.0, sass-loader@~7.3.1:
pify "^4.0.1" pify "^4.0.1"
semver "^6.3.0" semver "^6.3.0"
sass-loader@~8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.0.tgz#e7b07a3e357f965e6b03dd45b016b0a9746af797"
integrity sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==
dependencies:
clone-deep "^4.0.1"
loader-utils "^1.2.3"
neo-async "^2.6.1"
schema-utils "^2.1.0"
semver "^6.3.0"
sass-resources-loader@^2.0.0: sass-resources-loader@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.0.tgz#88569c542fbf1f18f33a6578b77cc5b36c56911d" resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.0.tgz#88569c542fbf1f18f33a6578b77cc5b36c56911d"
@ -15165,10 +15171,10 @@ vue-infinite-scroll@^2.0.2:
resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711" resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A== integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
vue-izitoast@^1.2.0: vue-izitoast@^1.2.1:
version "1.2.0" version "1.2.1"
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.0.tgz#55b7434a391c6eb64dd10c0de211e99ba7e486e2" resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.1.tgz#cd2cbfbd96ea438dede8fb00f2c328364cb7141d"
integrity sha512-Jqxfid12SUBIySJxgyPpu6gZ1ssMcbKtCvu9uMQPNM8RUnd3RKC4nyxkncdYe5L6XPU+SaznjYRudnvtclY4wA== integrity sha512-5krrKyAftSR3TnnO3zhMihYCSt0Lay4SBO1AWWKD3jhTErJrR+q9kOKyuAYhn1SttNER87hpnRKqdvLjzjHWQQ==
dependencies: dependencies:
izitoast "^1.4.0" izitoast "^1.4.0"

1113
yarn.lock

File diff suppressed because it is too large Load Diff