diff --git a/.travis.yml b/.travis.yml index 1cc36fca4..a2aae3cdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,13 +8,13 @@ addons: - docker - chromium -before_install: +install: - yarn global add wait-on # Install Codecov - yarn install - cp cypress.env.template.json cypress.env.json -install: +before_script: - docker-compose -f docker-compose.yml build --parallel - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d @@ -30,10 +30,6 @@ script: - docker-compose exec backend yarn run test --ci --verbose=false --coverage - docker-compose exec backend yarn run db:seed - docker-compose exec backend yarn run db:reset - # ActivityPub cucumber testing temporarily disabled because it's too buggy - # - docker-compose exec backend yarn run test:cucumber --tags "not @wip" - # - docker-compose exec backend yarn run db:reset - # - docker-compose exec backend yarn run db:seed # Frontend - docker-compose exec webapp yarn run lint - docker-compose exec webapp yarn run test --ci --verbose=false --coverage @@ -42,6 +38,7 @@ script: - docker-compose -f docker-compose.yml up -d - wait-on http://localhost:7474 - yarn run cypress:run --record + - yarn run cucumber # Coverage - yarn run codecov diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 000000000..2d91b3635 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "10" + } + } + ] + ] +} diff --git a/backend/.babelrc b/backend/babel.config.json similarity index 100% rename from backend/.babelrc rename to backend/babel.config.json diff --git a/backend/package.json b/backend/package.json index 7c66ba4a2..f641c528c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,8 +10,6 @@ "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql", "lint": "eslint src --config .eslintrc.js", "test": "jest --forceExit --detectOpenHandles --runInBand", - "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", - "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --", "db:reset": "babel-node src/seed/reset-db.js", "db:seed": "babel-node src/seed/seed-db.js" }, @@ -34,13 +32,13 @@ ] }, "dependencies": { - "@hapi/joi": "^16.1.7", + "@hapi/joi": "^16.1.8", "@sentry/node": "^5.9.0", "apollo-cache-inmemory": "~1.6.3", "apollo-client": "~2.6.4", "apollo-link-context": "~1.0.19", "apollo-link-http": "~1.5.16", - "apollo-server": "~2.9.11", + "apollo-server": "~2.9.12", "apollo-server-express": "^2.9.7", "babel-plugin-transform-runtime": "^6.23.0", "bcryptjs": "~2.4.3", @@ -57,7 +55,7 @@ "graphql-iso-date": "~3.6.1", "graphql-middleware": "~4.0.2", "graphql-middleware-sentry": "^3.2.1", - "graphql-shield": "~7.0.2", + "graphql-shield": "~7.0.4", "graphql-tag": "~2.10.1", "helmet": "~3.21.2", "jsonwebtoken": "~8.5.1", @@ -99,10 +97,10 @@ "xregexp": "^4.2.4" }, "devDependencies": { - "@babel/cli": "~7.7.0", - "@babel/core": "~7.7.2", + "@babel/cli": "~7.7.4", + "@babel/core": "~7.7.4", "@babel/node": "~7.7.4", - "@babel/plugin-proposal-throw-expressions": "^7.2.0", + "@babel/plugin-proposal-throw-expressions": "^7.7.4", "@babel/preset-env": "~7.7.4", "@babel/register": "~7.7.0", "apollo-server-testing": "~2.9.12", @@ -111,11 +109,11 @@ "babel-jest": "~24.9.0", "chai": "~4.2.0", "cucumber": "~6.0.5", - "eslint": "~6.7.1", + "eslint": "~6.7.2", "eslint-config-prettier": "~6.7.0", "eslint-config-standard": "~14.1.0", "eslint-plugin-import": "~2.18.2", - "eslint-plugin-jest": "~23.0.4", + "eslint-plugin-jest": "~23.1.1", "eslint-plugin-node": "~10.0.0", "eslint-plugin-prettier": "~3.1.1", "eslint-plugin-promise": "~4.2.1", diff --git a/backend/src/activitypub/routes/index.js b/backend/src/activitypub/routes/index.js index c7d31f1c4..fb4037004 100644 --- a/backend/src/activitypub/routes/index.js +++ b/backend/src/activitypub/routes/index.js @@ -1,27 +1,29 @@ import user from './user' import inbox from './inbox' -import webFinger from './webFinger' import express from 'express' import cors from 'cors' import verify from './verify' -const router = express.Router() - -router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger) -router.use( - '/activitypub/users', - cors(), - express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }), - express.urlencoded({ extended: true }), - user, -) -router.use( - '/activitypub/inbox', - cors(), - express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }), - express.urlencoded({ extended: true }), - verify, - inbox, -) - -export default router +export default function() { + const router = express.Router() + router.use( + '/activitypub/users', + cors(), + express.json({ + type: ['application/activity+json', 'application/ld+json', 'application/json'], + }), + express.urlencoded({ extended: true }), + user, + ) + router.use( + '/activitypub/inbox', + cors(), + express.json({ + type: ['application/activity+json', 'application/ld+json', 'application/json'], + }), + express.urlencoded({ extended: true }), + verify, + inbox, + ) + return router +} diff --git a/backend/src/activitypub/routes/webFinger.js b/backend/src/activitypub/routes/webFinger.js deleted file mode 100644 index 7d52c69cd..000000000 --- a/backend/src/activitypub/routes/webFinger.js +++ /dev/null @@ -1,43 +0,0 @@ -import express from 'express' -import { createWebFinger } from '../utils/actor' -import gql from 'graphql-tag' - -const router = express.Router() - -router.get('/', async function(req, res) { - const resource = req.query.resource - if (!resource || !resource.includes('acct:')) { - return res - .status(400) - .send( - 'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.', - ) - } else { - const nameAndDomain = resource.replace('acct:', '') - const name = nameAndDomain.split('@')[0] - - let result - try { - result = await req.app.get('ap').dataSource.client.query({ - query: gql` - query { - User(slug: "${name}") { - slug - } - } - `, - }) - } catch (error) { - return res.status(500).json({ error }) - } - - if (result.data && result.data.User.length > 0) { - const webFinger = createWebFinger(name) - return res.contentType('application/jrd+json').json(webFinger) - } else { - return res.status(404).json({ error: `No record found for ${nameAndDomain}.` }) - } - } -}) - -export default router diff --git a/backend/src/activitypub/routes/webfinger.js b/backend/src/activitypub/routes/webfinger.js new file mode 100644 index 000000000..c2cb96a6d --- /dev/null +++ b/backend/src/activitypub/routes/webfinger.js @@ -0,0 +1,59 @@ +import express from 'express' +import CONFIG from '../../config/' +import cors from 'cors' + +const debug = require('debug')('ea:webfinger') +const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/ + +const createWebFinger = name => { + const { host } = new URL(CONFIG.CLIENT_URI) + return { + subject: `acct:${name}@${host}`, + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`, + }, + ], + } +} + +export async function handler(req, res) { + const { resource = '' } = req.query + // eslint-disable-next-line no-unused-vars + const [_, name, domain] = resource.match(regex) || [] + if (!(name && domain)) + return res.status(400).json({ + error: 'Query parameter "?resource=acct:@" is missing.', + }) + + const session = req.app.get('driver').session() + try { + const [slug] = await session.readTransaction(async t => { + const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', { + slug: name, + }) + return result.records.map(record => record.get('slug')) + }) + if (!slug) + return res.status(404).json({ + error: `No record found for "${name}@${domain}".`, + }) + const webFinger = createWebFinger(name) + return res.contentType('application/jrd+json').json(webFinger) + } catch (error) { + debug(error) + return res.status(500).json({ + error: 'Something went terribly wrong. Please contact support@human-connection.org', + }) + } finally { + session.close() + } +} + +export default function() { + const router = express.Router() + router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler) + return router +} diff --git a/backend/src/activitypub/routes/webfinger.spec.js b/backend/src/activitypub/routes/webfinger.spec.js new file mode 100644 index 000000000..4e9b2196d --- /dev/null +++ b/backend/src/activitypub/routes/webfinger.spec.js @@ -0,0 +1,113 @@ +import { handler } from './webfinger' +import Factory from '../../seed/factories' +import { getDriver } from '../../bootstrap/neo4j' + +let resource, res, json, status, contentType + +const factory = Factory() +const driver = getDriver() + +const request = () => { + json = jest.fn() + status = jest.fn(() => ({ json })) + contentType = jest.fn(() => ({ json })) + res = { status, contentType } + const req = { + app: { + get: key => { + return { + driver, + }[key] + }, + }, + query: { + resource, + }, + } + return handler(req, res) +} + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('webfinger', () => { + describe('no ressource', () => { + beforeEach(() => { + resource = undefined + }) + + it('sends HTTP 400', async () => { + await request() + expect(status).toHaveBeenCalledWith(400) + expect(json).toHaveBeenCalledWith({ + error: 'Query parameter "?resource=acct:@" is missing.', + }) + }) + }) + + describe('?resource query param', () => { + describe('is missing acct:', () => { + beforeEach(() => { + resource = 'some-user@domain' + }) + + it('sends HTTP 400', async () => { + await request() + expect(status).toHaveBeenCalledWith(400) + expect(json).toHaveBeenCalledWith({ + error: 'Query parameter "?resource=acct:@" is missing.', + }) + }) + }) + + describe('has no domain', () => { + beforeEach(() => { + resource = 'acct:some-user@' + }) + + it('sends HTTP 400', async () => { + await request() + expect(status).toHaveBeenCalledWith(400) + expect(json).toHaveBeenCalledWith({ + error: 'Query parameter "?resource=acct:@" is missing.', + }) + }) + }) + + describe('with acct:', () => { + beforeEach(() => { + resource = 'acct:some-user@domain' + }) + + it('returns error as json', async () => { + await request() + expect(status).toHaveBeenCalledWith(404) + expect(json).toHaveBeenCalledWith({ + error: 'No record found for "some-user@domain".', + }) + }) + + describe('given a user for acct', () => { + beforeEach(async () => { + await factory.create('User', { slug: 'some-user' }) + }) + + it('returns user object', async () => { + await request() + expect(contentType).toHaveBeenCalledWith('application/jrd+json') + expect(json).toHaveBeenCalledWith({ + links: [ + { + href: 'http://localhost:3000/activitypub/users/some-user', + rel: 'self', + type: 'application/activity+json', + }, + ], + subject: 'acct:some-user@localhost:3000', + }) + }) + }) + }) + }) +}) diff --git a/backend/src/activitypub/utils/actor.js b/backend/src/activitypub/utils/actor.js index a08065778..e07397bdc 100644 --- a/backend/src/activitypub/utils/actor.js +++ b/backend/src/activitypub/utils/actor.js @@ -22,17 +22,3 @@ export function createActor(name, pubkey) { }, } } - -export function createWebFinger(name) { - const { host } = new URL(activityPub.endpoint) - return { - subject: `acct:${name}@${host}`, - links: [ - { - rel: 'self', - type: 'application/activity+json', - href: `${activityPub.endpoint}/activitypub/users/${name}`, - }, - ], - } -} diff --git a/backend/src/config/index.js b/backend/src/config/index.js index b67cea0f9..80573e180 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -1,6 +1,7 @@ import dotenv from 'dotenv' +import path from 'path' -dotenv.config() +dotenv.config({ path: path.resolve(__dirname, '../../.env') }) const { MAPBOX_TOKEN, diff --git a/backend/src/jwt/decode.js b/backend/src/jwt/decode.js index 842f8f537..5b7881d20 100644 --- a/backend/src/jwt/decode.js +++ b/backend/src/jwt/decode.js @@ -11,15 +11,21 @@ export default async (driver, authorizationHeader) => { } catch (err) { return null } - const session = driver.session() const query = ` MATCH (user:User {id: $id, deleted: false, disabled: false }) SET user.lastActiveAt = toString(datetime()) RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId} LIMIT 1 ` - const result = await session.run(query, { id }) - session.close() + const session = driver.session() + let result + + try { + result = await session.run(query, { id }) + } finally { + session.close() + } + const [currentUser] = await result.records.map(record => { return record.get('user') }) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.js b/backend/src/middleware/hashtags/hashtagsMiddleware.js index c9156398d..53a8fed20 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.js @@ -3,7 +3,6 @@ import extractHashtags from '../hashtags/extractHashtags' const updateHashtagsOfPost = async (postId, hashtags, context) => { if (!hashtags.length) return - const session = context.driver.session() // We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement // functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted // and no new Hashtags and relations will be created. @@ -19,14 +18,18 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => { MERGE (p)-[:TAGGED]->(t) RETURN p, t ` - await session.run(cypherDeletePreviousRelations, { - postId, - }) - await session.run(cypherCreateNewTagsAndRelations, { - postId, - hashtags, - }) - session.close() + const session = context.driver.session() + try { + await session.run(cypherDeletePreviousRelations, { + postId, + }) + await session.run(cypherCreateNewTagsAndRelations, { + postId, + hashtags, + }) + } finally { + session.close() + } } const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index 718f0b1e4..ac199a67d 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -1,15 +1,19 @@ import extractMentionedUsers from './mentions/extractMentionedUsers' const postAuthorOfComment = async (comment, { context }) => { - const session = context.driver.session() const cypherFindUser = ` MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) RETURN user { .id } ` - const result = await session.run(cypherFindUser, { - commentId: comment.id, - }) - session.close() + const session = context.driver.session() + let result + try { + result = await session.run(cypherFindUser, { + commentId: comment.id, + }) + } finally { + session.close() + } const [postAuthor] = await result.records.map(record => { return record.get('user') }) @@ -31,7 +35,6 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { throw new Error('Notification does not fit the reason!') } - const session = context.driver.session() let cypher switch (reason) { case 'mentioned_in_post': { @@ -85,12 +88,16 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { break } } - await session.run(cypher, { - id, - idsOfUsers, - reason, - }) - session.close() + const session = context.driver.session() + try { + await session.run(cypher, { + id, + idsOfUsers, + reason, + }) + } finally { + session.close() + } } const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { @@ -123,15 +130,19 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo) if (comment) { - const session = context.driver.session() const cypherFindUser = ` MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) RETURN user { .id } ` - const result = await session.run(cypherFindUser, { - commentId: comment.id, - }) - session.close() + const session = context.driver.session() + let result + try { + result = await session.run(cypherFindUser, { + commentId: comment.id, + }) + } finally { + session.close() + } const [postAuthor] = await result.records.map(record => { return record.get('user') }) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 4622c3f24..8e4569a52 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, deny, allow, and, or, not } from 'graphql-shield' +import { rule, shield, deny, allow, or } from 'graphql-shield' import { neode } from '../bootstrap/neo4j' import CONFIG from '../config' @@ -41,48 +41,27 @@ const isMySocialMedia = rule({ return socialMedia.ownedBy.node.id === user.id }) -const invitationLimitReached = rule({ - cache: 'no_cache', -})(async (parent, args, { user, driver }) => { - const session = driver.session() - try { - const result = await session.run( - ` - MATCH (user:User {id:$id})-[:GENERATED]->(i:InvitationCode) - RETURN COUNT(i) >= 3 as limitReached - `, - { id: user.id }, - ) - const [limitReached] = result.records.map(record => { - return record.get('limitReached') - }) - return limitReached - } finally { - session.close() - } -}) - const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { if (!user) return false - const session = driver.session() const { id: resourceId } = args - const result = await session.run( - ` - MATCH (resource {id: $resourceId})<-[:WROTE]-(author) - RETURN author - `, - { - resourceId, - }, - ) - session.close() - const [author] = result.records.map(record => { - return record.get('author') - }) - const authorId = author && author.properties && author.properties.id - return authorId === user.id + const session = driver.session() + try { + const result = await session.run( + ` + MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId}) + RETURN author + `, + { resourceId, userId: user.id }, + ) + const [author] = result.records.map(record => { + return record.get('author') + }) + return !!author + } finally { + session.close() + } }) const isDeletingOwnAccount = rule({ @@ -129,12 +108,11 @@ export default shield( SignupByInvitation: allow, Signup: or(publicRegistration, isAdmin), SignupVerification: allow, - CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)), UpdateUser: onlyYourself, CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, - report: isAuthenticated, + fileReport: isAuthenticated, CreateSocialMedia: isAuthenticated, UpdateSocialMedia: isMySocialMedia, DeleteSocialMedia: isMySocialMedia, @@ -147,8 +125,7 @@ export default shield( shout: isAuthenticated, unshout: isAuthenticated, changePassword: isAuthenticated, - enable: isModerator, - disable: isModerator, + review: isModerator, CreateComment: isAuthenticated, UpdateComment: isAuthor, DeleteComment: isAuthor, diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index fed9b4da7..cda3fd335 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -3,11 +3,14 @@ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { return async slug => { const session = context.driver.session() - const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, { - slug, - }) - session.close() - return response.records.length === 0 + try { + const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, { + slug, + }) + return response.records.length === 0 + } finally { + session.close() + } } } diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index 6da080ebb..1c97cb874 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -8,14 +8,8 @@ const factory = Factory() const neode = getNeode() const driver = getDriver() -let query -let mutate -let graphqlQuery const categoryIds = ['cat9'] -let authenticatedUser -let user -let moderator -let troll +let query, graphqlQuery, authenticatedUser, user, moderator, troll const action = () => { return query({ query: graphqlQuery }) @@ -38,18 +32,17 @@ beforeAll(async () => { avatar: '/some/offensive/avatar.jpg', about: 'This self description is very offensive', }), + neode.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }), ]) user = users[0] moderator = users[1] troll = users[2] - await neode.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) - await Promise.all([ user.relateTo(troll, 'following'), factory.create('Post', { @@ -70,33 +63,32 @@ beforeAll(async () => { }), ]) - await Promise.all([ + const resources = await Promise.all([ factory.create('Comment', { author: user, id: 'c2', postId: 'p3', content: 'Enabled comment on public post', }), + factory.create('Post', { + id: 'p2', + author: troll, + title: 'Disabled post', + content: 'This is an offensive post content', + contentExcerpt: 'This is an offensive post content', + image: '/some/offensive/image.jpg', + deleted: false, + categoryIds, + }), + factory.create('Comment', { + id: 'c1', + author: troll, + postId: 'p3', + content: 'Disabled comment', + contentExcerpt: 'Disabled comment', + }), ]) - await factory.create('Post', { - id: 'p2', - author: troll, - title: 'Disabled post', - content: 'This is an offensive post content', - contentExcerpt: 'This is an offensive post content', - image: '/some/offensive/image.jpg', - deleted: false, - categoryIds, - }) - await factory.create('Comment', { - id: 'c1', - author: troll, - postId: 'p3', - content: 'Disabled comment', - contentExcerpt: 'Disabled comment', - }) - const { server } = createServer({ context: () => { return { @@ -108,20 +100,57 @@ beforeAll(async () => { }) const client = createTestClient(server) query = client.query - mutate = client.mutate - authenticatedUser = await moderator.toJson() - const disableMutation = gql` - mutation($id: ID!) { - disable(id: $id) - } - ` - await Promise.all([ - mutate({ mutation: disableMutation, variables: { id: 'c1' } }), - mutate({ mutation: disableMutation, variables: { id: 'u2' } }), - mutate({ mutation: disableMutation, variables: { id: 'p2' } }), + const trollingPost = resources[1] + const trollingComment = resources[2] + + const reports = await Promise.all([ + factory.create('Report'), + factory.create('Report'), + factory.create('Report'), + ]) + const reportAgainstTroll = reports[0] + const reportAgainstTrollingPost = reports[1] + const reportAgainstTrollingComment = reports[2] + + const reportVariables = { + resourceId: 'undefined-resource', + reasonCategory: 'discrimination_etc', + reasonDescription: 'I am what I am !!!', + } + + await Promise.all([ + reportAgainstTroll.relateTo(user, 'filed', { ...reportVariables, resourceId: 'u2' }), + reportAgainstTroll.relateTo(troll, 'belongsTo'), + reportAgainstTrollingPost.relateTo(user, 'filed', { ...reportVariables, resourceId: 'p2' }), + reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'), + reportAgainstTrollingComment.relateTo(moderator, 'filed', { + ...reportVariables, + resourceId: 'c1', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + + const disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + + await Promise.all([ + reportAgainstTroll.relateTo(moderator, 'reviewed', { ...disableVariables, resourceId: 'u2' }), + troll.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingPost.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'p2', + }), + trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingComment.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), ]) - authenticatedUser = null }) afterAll(async () => { diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index bd4805ed8..f36458e61 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -5,7 +5,7 @@ const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' const NO_CATEGORIES_ERR_MESSAGE = 'You cannot save a post without at least one category or more than three' -const validateCommentCreation = async (resolve, root, args, context, info) => { +const validateCreateComment = async (resolve, root, args, context, info) => { const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() const { postId } = args @@ -13,28 +13,30 @@ const validateCommentCreation = async (resolve, root, args, context, info) => { throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) } const session = context.driver.session() - const postQueryRes = await session.run( - ` + try { + const postQueryRes = await session.run( + ` MATCH (post:Post {id: $postId}) RETURN post`, - { - postId, - }, - ) - session.close() - const [post] = postQueryRes.records.map(record => { - return record.get('post') - }) + { + postId, + }, + ) + const [post] = postQueryRes.records.map(record => { + return record.get('post') + }) - if (!post) { - throw new UserInputError(NO_POST_ERR_MESSAGE) - } else { - return resolve(root, args, context, info) + if (!post) { + throw new UserInputError(NO_POST_ERR_MESSAGE) + } else { + return resolve(root, args, context, info) + } + } finally { + session.close() } } const validateUpdateComment = async (resolve, root, args, context, info) => { - const COMMENT_MIN_LENGTH = 1 const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() if (!args.content || content.length < COMMENT_MIN_LENGTH) { throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) @@ -59,36 +61,67 @@ const validateUpdatePost = async (resolve, root, args, context, info) => { const validateReport = async (resolve, root, args, context, info) => { const { resourceId } = args - const { user, driver } = context + const { user } = context if (resourceId === user.id) throw new Error('You cannot report yourself!') - const session = driver.session() - const reportQueryRes = await session.run( - ` - MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) - RETURN labels(resource)[0] as label - `, - { - resourceId, - submitterId: user.id, - }, - ) - session.close() - const [existingReportedResource] = reportQueryRes.records.map(record => { - return { - label: record.get('label'), - } - }) + return resolve(root, args, context, info) +} + +const validateReview = async (resolve, root, args, context, info) => { + const { resourceId } = args + let existingReportedResource + const { user, driver } = context + if (resourceId === user.id) throw new Error('You cannot review yourself!') + const session = driver.session() + const reportReadTxPromise = session.writeTransaction(async txc => { + const validateReviewTransactionResponse = await txc.run( + ` + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Post OR resource:Comment + OPTIONAL MATCH (:User)-[filed:FILED]->(:Report {closed: false})-[:BELONGS_TO]->(resource) + OPTIONAL MATCH (resource)<-[:WROTE]-(author:User) + RETURN labels(resource)[0] AS label, author, filed + `, + { + resourceId, + submitterId: user.id, + }, + ) + return validateReviewTransactionResponse.records.map(record => ({ + label: record.get('label'), + author: record.get('author'), + filed: record.get('filed'), + })) + }) + try { + const txResult = await reportReadTxPromise + existingReportedResource = txResult + if (!existingReportedResource || !existingReportedResource.length) + throw new Error(`Resource not found or is not a Post|Comment|User!`) + existingReportedResource = existingReportedResource[0] + if (!existingReportedResource.filed) + throw new Error( + `Before starting the review process, please report the ${existingReportedResource.label}!`, + ) + const authorId = + existingReportedResource.label !== 'User' && existingReportedResource.author + ? existingReportedResource.author.properties.id + : null + if (authorId && authorId === user.id) + throw new Error(`You cannot review your own ${existingReportedResource.label}!`) + } finally { + session.close() + } - if (existingReportedResource) throw new Error(`${existingReportedResource.label}`) return resolve(root, args, context, info) } export default { Mutation: { - CreateComment: validateCommentCreation, + CreateComment: validateCreateComment, UpdateComment: validateUpdateComment, CreatePost: validatePost, UpdatePost: validateUpdatePost, - report: validateReport, + fileReport: validateReport, + review: validateReview, }, } diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js new file mode 100644 index 000000000..97bb6254b --- /dev/null +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -0,0 +1,400 @@ +import { gql } from '../../helpers/jest' +import Factory from '../../seed/factories' +import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { createTestClient } from 'apollo-server-testing' +import createServer from '../../server' + +const factory = Factory() +const neode = getNeode() +const driver = getDriver() +let authenticatedUser, + mutate, + users, + offensivePost, + reportVariables, + disableVariables, + reportingUser, + moderatingUser, + commentingUser + +const createCommentMutation = gql` + mutation($id: ID, $postId: ID!, $content: String!) { + CreateComment(id: $id, postId: $postId, content: $content) { + id + } + } +` +const updateCommentMutation = gql` + mutation($content: String!, $id: ID!) { + UpdateComment(content: $content, id: $id) { + id + } + } +` +const createPostMutation = gql` + mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) { + CreatePost( + id: $id + title: $title + content: $content + language: $language + categoryIds: $categoryIds + ) { + id + } + } +` + +const updatePostMutation = gql` + mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) { + UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { + id + } + } +` +const reportMutation = gql` + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + fileReport( + resourceId: $resourceId + reasonCategory: $reasonCategory + reasonDescription: $reasonDescription + ) { + id + } + } +` +const reviewMutation = gql` + mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) { + review(resourceId: $resourceId, disable: $disable, closed: $closed) { + createdAt + updatedAt + } + } +` +beforeAll(() => { + const { server } = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + } + }, + }) + mutate = createTestClient(server).mutate +}) + +beforeEach(async () => { + users = await Promise.all([ + factory.create('User', { + id: 'reporting-user', + }), + factory.create('User', { + id: 'moderating-user', + role: 'moderator', + }), + factory.create('User', { + id: 'commenting-user', + }), + ]) + reportVariables = { + resourceId: 'whatever', + reasonCategory: 'other', + reasonDescription: 'Violates code of conduct !!!', + } + disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + reportingUser = users[0] + moderatingUser = users[1] + commentingUser = users[2] + const posts = await Promise.all([ + factory.create('Post', { + id: 'offensive-post', + authorId: 'moderating-user', + }), + factory.create('Post', { + id: 'post-4-commenting', + authorId: 'commenting-user', + }), + ]) + offensivePost = posts[0] +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('validateCreateComment', () => { + let createCommentVariables + beforeEach(async () => { + createCommentVariables = { + postId: 'whatever', + content: '', + } + authenticatedUser = await commentingUser.toJson() + }) + + it('throws an error if content is empty', async () => { + createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' } + await expect( + mutate({ mutation: createCommentMutation, variables: createCommentVariables }), + ).resolves.toMatchObject({ + data: { CreateComment: null }, + errors: [{ message: 'Comment must be at least 1 character long!' }], + }) + }) + + it('sanitizes content and throws an error if not longer than 1 character', async () => { + createCommentVariables = { postId: 'post-4-commenting', content: '' } + await expect( + mutate({ mutation: createCommentMutation, variables: createCommentVariables }), + ).resolves.toMatchObject({ + data: { CreateComment: null }, + errors: [{ message: 'Comment must be at least 1 character long!' }], + }) + }) + + it('throws an error if there is no post with given id in the database', async () => { + createCommentVariables = { + ...createCommentVariables, + postId: 'non-existent-post', + content: 'valid content', + } + await expect( + mutate({ mutation: createCommentMutation, variables: createCommentVariables }), + ).resolves.toMatchObject({ + data: { CreateComment: null }, + errors: [{ message: 'Comment cannot be created without a post!' }], + }) + }) + + describe('validateUpdateComment', () => { + let updateCommentVariables + beforeEach(async () => { + await factory.create('Comment', { + id: 'comment-id', + authorId: 'commenting-user', + }) + updateCommentVariables = { + id: 'whatever', + content: '', + } + authenticatedUser = await commentingUser.toJson() + }) + + it('throws an error if content is empty', async () => { + updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' } + await expect( + mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }), + ).resolves.toMatchObject({ + data: { UpdateComment: null }, + errors: [{ message: 'Comment must be at least 1 character long!' }], + }) + }) + + it('sanitizes content and throws an error if not longer than 1 character', async () => { + updateCommentVariables = { id: 'comment-id', content: '' } + await expect( + mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }), + ).resolves.toMatchObject({ + data: { UpdateComment: null }, + errors: [{ message: 'Comment must be at least 1 character long!' }], + }) + }) + }) + + describe('validatePost', () => { + let createPostVariables + beforeEach(async () => { + createPostVariables = { + title: 'I am a title', + content: 'Some content', + } + authenticatedUser = await commentingUser.toJson() + }) + + describe('categories', () => { + describe('null', () => { + it('throws UserInputError', async () => { + createPostVariables = { ...createPostVariables, categoryIds: null } + await expect( + mutate({ mutation: createPostMutation, variables: createPostVariables }), + ).resolves.toMatchObject({ + data: { CreatePost: null }, + errors: [ + { + message: 'You cannot save a post without at least one category or more than three', + }, + ], + }) + }) + }) + + describe('empty', () => { + it('throws UserInputError', async () => { + createPostVariables = { ...createPostVariables, categoryIds: [] } + await expect( + mutate({ mutation: createPostMutation, variables: createPostVariables }), + ).resolves.toMatchObject({ + data: { CreatePost: null }, + errors: [ + { + message: 'You cannot save a post without at least one category or more than three', + }, + ], + }) + }) + }) + + describe('more than 3 categoryIds', () => { + it('throws UserInputError', async () => { + createPostVariables = { + ...createPostVariables, + categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'], + } + await expect( + mutate({ mutation: createPostMutation, variables: createPostVariables }), + ).resolves.toMatchObject({ + data: { CreatePost: null }, + errors: [ + { + message: 'You cannot save a post without at least one category or more than three', + }, + ], + }) + }) + }) + }) + }) + + describe('validateUpdatePost', () => { + describe('post created without categories somehow', () => { + let owner, updatePostVariables + beforeEach(async () => { + const postSomehowCreated = await neode.create('Post', { + id: 'how-was-this-created', + }) + owner = await neode.create('User', { + id: 'author-of-post-without-category', + slug: 'hacker', + }) + await postSomehowCreated.relateTo(owner, 'author') + authenticatedUser = await owner.toJson() + updatePostVariables = { + id: 'how-was-this-created', + title: 'I am a title', + content: 'Some content', + categoryIds: [], + } + }) + + it('requires at least one category for successful update', async () => { + await expect( + mutate({ mutation: updatePostMutation, variables: updatePostVariables }), + ).resolves.toMatchObject({ + data: { UpdatePost: null }, + errors: [ + { message: 'You cannot save a post without at least one category or more than three' }, + ], + }) + }) + }) + }) +}) + +describe('validateReport', () => { + it('throws an error if a user tries to report themself', async () => { + authenticatedUser = await reportingUser.toJson() + reportVariables = { ...reportVariables, resourceId: 'reporting-user' } + await expect( + mutate({ mutation: reportMutation, variables: reportVariables }), + ).resolves.toMatchObject({ + data: { fileReport: null }, + errors: [{ message: 'You cannot report yourself!' }], + }) + }) +}) + +describe('validateReview', () => { + beforeEach(async () => { + const reportAgainstModerator = await factory.create('Report') + await Promise.all([ + reportAgainstModerator.relateTo(reportingUser, 'filed', { + ...reportVariables, + resourceId: 'moderating-user', + }), + reportAgainstModerator.relateTo(moderatingUser, 'belongsTo'), + ]) + authenticatedUser = await moderatingUser.toJson() + }) + + it('throws an error if a user tries to review a report against them', async () => { + disableVariables = { ...disableVariables, resourceId: 'moderating-user' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'You cannot review yourself!' }], + }) + }) + + it('throws an error for invaild resource', async () => { + disableVariables = { ...disableVariables, resourceId: 'non-existent-resource' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], + }) + }) + + it('throws an error if no report exists', async () => { + disableVariables = { ...disableVariables, resourceId: 'offensive-post' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Before starting the review process, please report the Post!' }], + }) + }) + + it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => { + const reportAgainstOffensivePost = await factory.create('Report') + await Promise.all([ + reportAgainstOffensivePost.relateTo(reportingUser, 'filed', { + ...reportVariables, + resourceId: 'offensive-post', + }), + reportAgainstOffensivePost.relateTo(offensivePost, 'belongsTo'), + ]) + disableVariables = { ...disableVariables, resourceId: 'offensive-post' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'You cannot review your own Post!' }], + }) + }) + + describe('moderate a resource that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + await Promise.all([factory.create('Tag', { id: 'tag-id' })]) + }) + + it('returns null', async () => { + disableVariables = { + ...disableVariables, + resourceId: 'tag-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], + }) + }) + }) +}) diff --git a/backend/src/models/Comment.js b/backend/src/models/Comment.js index c89103e5d..54cbda675 100644 --- a/backend/src/models/Comment.js +++ b/backend/src/models/Comment.js @@ -25,12 +25,6 @@ module.exports = { target: 'User', direction: 'in', }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, notified: { type: 'relationship', relationship: 'NOTIFIED', diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index 7c287680c..18dc0e464 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -17,12 +17,6 @@ module.exports = { image: { type: 'string', allow: [null] }, deleted: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, notified: { type: 'relationship', relationship: 'NOTIFIED', diff --git a/backend/src/models/Report.js b/backend/src/models/Report.js new file mode 100644 index 000000000..b66aa4076 --- /dev/null +++ b/backend/src/models/Report.js @@ -0,0 +1,53 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + rule: { type: 'string', default: 'latestReviewUpdatedAtRules' }, + disable: { type: 'boolean', default: false }, + closed: { type: 'boolean', default: false }, + belongsTo: { + type: 'relationship', + relationship: 'BELONGS_TO', + target: ['User', 'Comment', 'Post'], + direction: 'out', + }, + filed: { + type: 'relationship', + relationship: 'FILED', + target: 'User', + direction: 'in', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + resourceId: { type: 'string', primary: true, default: uuid }, + reasonCategory: { + type: 'string', + valid: [ + 'other', + 'discrimination_etc', + 'pornographic_content_links', + 'glorific_trivia_of_cruel_inhuman_acts', + 'doxing', + 'intentional_intimidation_stalking_persecution', + 'advert_products_services_commercial', + 'criminal_behavior_violation_german_law', + ], + invalid: [null], + }, + reasonDescription: { type: 'string', allow: [null] }, + }, + }, + reviewed: { + type: 'relationship', + relationship: 'REVIEWED', + target: 'User', + direction: 'in', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + disable: { type: 'boolean', default: false }, + closed: { type: 'boolean', default: false }, + }, + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js index fd6e88c27..32f053e2b 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -42,12 +42,6 @@ module.exports = { }, }, friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, rewarded: { type: 'relationship', relationship: 'REWARDED', diff --git a/backend/src/models/index.js b/backend/src/models/index.js index bd89ddc51..0b9378162 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -3,7 +3,6 @@ export default { Badge: require('./Badge.js'), User: require('./User.js'), - InvitationCode: require('./InvitationCode.js'), EmailAddress: require('./EmailAddress.js'), UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'), SocialMedia: require('./SocialMedia.js'), @@ -13,4 +12,5 @@ export default { Tag: require('./Tag.js'), Location: require('./Location.js'), Donations: require('./Donations.js'), + Report: require('./Report.js'), } diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 516f47abd..274697238 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -10,7 +10,6 @@ export default makeAugmentedSchema({ exclude: [ 'Badge', 'Embed', - 'InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', @@ -18,7 +17,9 @@ export default makeAugmentedSchema({ 'Location', 'SocialMedia', 'NOTIFIED', - 'REPORTED', + 'FILED', + 'REVIEWED', + 'Report', 'Donations', ], }, diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index e0b69b153..97b461511 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -13,7 +13,8 @@ export default { params.id = params.id || uuid() const session = context.driver.session() - const createCommentCypher = ` + try { + const createCommentCypher = ` MATCH (post:Post {id: $postId}) MATCH (author:User {id: $userId}) WITH post, author @@ -23,45 +24,53 @@ export default { MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author) RETURN comment ` - const transactionRes = await session.run(createCommentCypher, { - userId: context.user.id, - postId, - params, - }) - session.close() + const transactionRes = await session.run(createCommentCypher, { + userId: context.user.id, + postId, + params, + }) - const [comment] = transactionRes.records.map(record => record.get('comment').properties) + const [comment] = transactionRes.records.map(record => record.get('comment').properties) - return comment + return comment + } finally { + session.close() + } }, UpdateComment: async (_parent, params, context, _resolveInfo) => { const session = context.driver.session() - const updateCommentCypher = ` + try { + const updateCommentCypher = ` MATCH (comment:Comment {id: $params.id}) SET comment += $params SET comment.updatedAt = toString(datetime()) RETURN comment ` - const transactionRes = await session.run(updateCommentCypher, { params }) - session.close() - const [comment] = transactionRes.records.map(record => record.get('comment').properties) - return comment + const transactionRes = await session.run(updateCommentCypher, { params }) + const [comment] = transactionRes.records.map(record => record.get('comment').properties) + return comment + } finally { + session.close() + } }, DeleteComment: async (_parent, args, context, _resolveInfo) => { const session = context.driver.session() - const transactionRes = await session.run( - ` + try { + const transactionRes = await session.run( + ` MATCH (comment:Comment {id: $commentId}) SET comment.deleted = TRUE SET comment.content = 'UNAVAILABLE' SET comment.contentExcerpt = 'UNAVAILABLE' RETURN comment `, - { commentId: args.id }, - ) - session.close() - const [comment] = transactionRes.records.map(record => record.get('comment').properties) - return comment + { commentId: args.id }, + ) + const [comment] = transactionRes.records.map(record => record.get('comment').properties) + return comment + } finally { + session.close() + } }, }, Comment: { @@ -69,7 +78,6 @@ export default { hasOne: { author: '<-[:WROTE]-(related:User)', post: '-[:COMMENTS]->(related:Post)', - disabledBy: '<-[:DISABLED]-(related:User)', }, }), }, diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index c0f86ffe3..d2692aa8a 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -111,42 +111,6 @@ describe('CreateComment', () => { }, ) }) - - describe('comment content is empty', () => { - beforeEach(() => { - variables = { ...variables, content: '

' } - }) - - it('throw UserInput error', async () => { - const { data, errors } = await mutate({ mutation: createCommentMutation, variables }) - expect(data).toEqual({ CreateComment: null }) - expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!') - }) - }) - - describe('comment content contains only whitespaces', () => { - beforeEach(() => { - variables = { ...variables, content: '

' } - }) - - it('throw UserInput error', async () => { - const { data, errors } = await mutate({ mutation: createCommentMutation, variables }) - expect(data).toEqual({ CreateComment: null }) - expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!') - }) - }) - - describe('invalid post id', () => { - beforeEach(() => { - variables = { ...variables, postId: 'does-not-exist' } - }) - - it('throw UserInput error', async () => { - const { data, errors } = await mutate({ mutation: createCommentMutation, variables }) - expect(data).toEqual({ CreateComment: null }) - expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!') - }) - }) }) }) }) @@ -226,17 +190,6 @@ describe('UpdateComment', () => { expect(newlyCreatedComment.updatedAt).not.toEqual(UpdateComment.updatedAt) }) - describe('if `content` empty', () => { - beforeEach(() => { - variables = { ...variables, content: '

' } - }) - - it('throws InputError', async () => { - const { errors } = await mutate({ mutation: updateCommentMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!') - }) - }) - describe('if comment does not exist for given id', () => { beforeEach(() => { variables = { ...variables, id: 'does-not-exist' } diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js index 88149077d..3052ff13d 100644 --- a/backend/src/schema/resolvers/donations.js +++ b/backend/src/schema/resolvers/donations.js @@ -2,8 +2,8 @@ export default { Mutation: { UpdateDonations: async (_parent, params, context, _resolveInfo) => { const { driver } = context - const session = driver.session() let donations + const session = driver.session() const writeTxResultPromise = session.writeTransaction(async txc => { const updateDonationsTransactionResponse = await txc.run( ` diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js index 06c0dbd1a..8711a5996 100644 --- a/backend/src/schema/resolvers/emails.js +++ b/backend/src/schema/resolvers/emails.js @@ -3,7 +3,7 @@ import Resolver from './helpers/Resolver' import existingEmailAddress from './helpers/existingEmailAddress' import { UserInputError } from 'apollo-server' import Validator from 'neode/build/Services/Validator.js' -import { normalizeEmail } from 'validator' +import normalizeEmail from './helpers/normalizeEmail' export default { Mutation: { diff --git a/backend/src/schema/resolvers/helpers/createPasswordReset.js b/backend/src/schema/resolvers/helpers/createPasswordReset.js index 8d575abfc..41214b501 100644 --- a/backend/src/schema/resolvers/helpers/createPasswordReset.js +++ b/backend/src/schema/resolvers/helpers/createPasswordReset.js @@ -1,10 +1,9 @@ -import { normalizeEmail } from 'validator' +import normalizeEmail from './normalizeEmail' export default async function createPasswordReset(options) { const { driver, nonce, email, issuedAt = new Date() } = options const normalizedEmail = normalizeEmail(email) const session = driver.session() - let response = {} try { const cypher = ` MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) @@ -23,9 +22,8 @@ export default async function createPasswordReset(options) { const { name } = record.get('u').properties return { email, nonce, name } }) - response = records[0] || {} + return records[0] || {} } finally { session.close() } - return response } diff --git a/backend/src/schema/resolvers/helpers/normalizeEmail.js b/backend/src/schema/resolvers/helpers/normalizeEmail.js new file mode 100644 index 000000000..bdd12e991 --- /dev/null +++ b/backend/src/schema/resolvers/helpers/normalizeEmail.js @@ -0,0 +1,11 @@ +import { normalizeEmail } from 'validator' + +export default email => + normalizeEmail(email, { + // gmail_remove_dots: true, default + gmail_remove_subaddress: false, + // gmail_convert_googlemaildotcom: true, default + outlookdotcom_remove_subaddress: false, + yahoo_remove_subaddress: false, + icloud_remove_subaddress: false, + }) diff --git a/backend/src/schema/resolvers/moderation.js b/backend/src/schema/resolvers/moderation.js index d61df7545..4bdf82d50 100644 --- a/backend/src/schema/resolvers/moderation.js +++ b/backend/src/schema/resolvers/moderation.js @@ -1,41 +1,51 @@ +const transformReturnType = record => { + return { + ...record.get('review').properties, + report: record.get('report').properties, + resource: { + __typename: record.get('type'), + ...record.get('resource').properties, + }, + } +} + export default { Mutation: { - disable: async (object, params, { user, driver }) => { - const { id } = params - const { id: userId } = user - const cypher = ` - MATCH (u:User {id: $userId}) - MATCH (resource {id: $id}) - WHERE resource:User OR resource:Comment OR resource:Post - SET resource.disabled = true - MERGE (resource)<-[:DISABLED]-(u) - RETURN resource {.id} - ` + review: async (_object, params, context, _resolveInfo) => { + const { user: moderator, driver } = context + + let createdRelationshipWithNestedAttributes = null // return value const session = driver.session() - const res = await session.run(cypher, { id, userId }) - session.close() - const [resource] = res.records.map(record => { - return record.get('resource') - }) - if (!resource) return null - return resource.id - }, - enable: async (object, params, { user, driver }) => { - const { id } = params - const cypher = ` - MATCH (resource {id: $id})<-[d:DISABLED]-() - SET resource.disabled = false - DELETE d - RETURN resource {.id} - ` - const session = driver.session() - const res = await session.run(cypher, { id }) - session.close() - const [resource] = res.records.map(record => { - return record.get('resource') - }) - if (!resource) return null - return resource.id + try { + const cypher = ` + MATCH (moderator:User {id: $moderatorId}) + MATCH (resource {id: $params.resourceId})<-[:BELONGS_TO]-(report:Report {closed: false}) + WHERE resource:User OR resource:Post OR resource:Comment + MERGE (report)<-[review:REVIEWED]-(moderator) + ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt + ON MATCH SET review.updatedAt = $dateTime + SET review.disable = $params.disable + SET report.updatedAt = $dateTime, report.closed = $params.closed + SET resource.disabled = review.disable + + RETURN review, report, resource, labels(resource)[0] AS type + ` + const reviewWriteTxResultPromise = session.writeTransaction(async txc => { + const reviewTransactionResponse = await txc.run(cypher, { + params, + moderatorId: moderator.id, + dateTime: new Date().toISOString(), + }) + return reviewTransactionResponse.records.map(transformReturnType) + }) + const txResult = await reviewWriteTxResultPromise + if (!txResult[0]) return null + createdRelationshipWithNestedAttributes = txResult[0] + } finally { + session.close() + } + + return createdRelationshipWithNestedAttributes }, }, } diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index 765126c52..5e280a6f5 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -8,45 +8,53 @@ const factory = Factory() const neode = getNeode() const driver = getDriver() -let query, mutate, authenticatedUser, variables, moderator, nonModerator +let mutate, + authenticatedUser, + disableVariables, + enableVariables, + moderator, + nonModerator, + closeReportVariables -const disableMutation = gql` - mutation($id: ID!) { - disable(id: $id) - } -` -const enableMutation = gql` - mutation($id: ID!) { - enable(id: $id) - } -` - -const commentQuery = gql` - query($id: ID!) { - Comment(id: $id) { - id - disabled - disabledBy { - id +const reviewMutation = gql` + mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) { + review(resourceId: $resourceId, disable: $disable, closed: $closed) { + createdAt + updatedAt + resource { + __typename + ... on User { + id + disabled + } + ... on Post { + id + disabled + } + ... on Comment { + id + disabled + } } - } - } -` - -const postQuery = gql` - query($id: ID) { - Post(id: $id) { - id - disabled - disabledBy { + report { id + createdAt + updatedAt + closed + reviewed { + createdAt + moderator { + id + } + } } } } ` describe('moderate resources', () => { - beforeAll(() => { + beforeAll(async () => { + await factory.cleanDatabase() authenticatedUser = undefined const { server } = createServer({ context: () => { @@ -58,11 +66,19 @@ describe('moderate resources', () => { }, }) mutate = createTestClient(server).mutate - query = createTestClient(server).query }) beforeEach(async () => { - variables = {} + disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + enableVariables = { + resourceId: 'undefined-resource', + disable: false, + closed: false, + } authenticatedUser = null moderator = await factory.create('User', { id: 'moderator-id', @@ -71,155 +87,392 @@ describe('moderate resources', () => { password: '1234', role: 'moderator', }) + nonModerator = await factory.create('User', { + id: 'non-moderator', + name: 'Non Moderator', + email: 'non.moderator@example.org', + password: '1234', + }) }) afterEach(async () => { await factory.cleanDatabase() }) - describe('disable', () => { - beforeEach(() => { - variables = { - id: 'some-resource', - } - }) + describe('review to close report, leaving resource enabled', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ errors: [{ message: 'Not Authorised!' }], }) }) }) + describe('authenticated', () => { - describe('non moderator', () => { - beforeEach(async () => { - nonModerator = await factory.create('User', { - id: 'non-moderator', - name: 'Non Moderator', - email: 'non.moderator@example.org', - password: '1234', - }) - authenticatedUser = await nonModerator.toJson() + beforeEach(async () => { + authenticatedUser = await nonModerator.toJson() + }) + + it('non-moderator receives an authorization error', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], }) - it('throws authorization error', async () => { - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + }) + }) + + describe('moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + const questionablePost = await factory.create('Post', { + id: 'should-i-be-disabled', + }) + const reportAgainstQuestionablePost = await factory.create('Report') + await Promise.all([ + reportAgainstQuestionablePost.relateTo(nonModerator, 'filed', { + resourceId: 'should-i-be-disabled', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", + }), + reportAgainstQuestionablePost.relateTo(questionablePost, 'belongsTo'), + ]) + closeReportVariables = { + resourceId: 'should-i-be-disabled', + disable: false, + closed: true, + } + }) + + it('report can be closed without disabling resource', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'should-i-be-disabled', disabled: false }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, + }) + }) + + it('creates only one review for multiple reviews by the same moderator on same resource', async () => { + await Promise.all([ + mutate({ + mutation: reviewMutation, + variables: { ...disableVariables, resourceId: 'should-i-be-disabled' }, + }), + mutate({ + mutation: reviewMutation, + variables: { ...enableVariables, resourceId: 'should-i-be-disabled' }, + }), + ]) + const cypher = + 'MATCH (:Report)<-[review:REVIEWED]-(moderator:User {id: "moderator-id"}) RETURN review' + const reviews = await neode.cypher(cypher) + expect(reviews.records).toHaveLength(1) + }) + + it('updates the updatedAt attribute', async () => { + const [firstReview, secondReview] = await Promise.all([ + mutate({ + mutation: reviewMutation, + variables: { ...disableVariables, resourceId: 'should-i-be-disabled' }, + }), + mutate({ + mutation: reviewMutation, + variables: { ...enableVariables, resourceId: 'should-i-be-disabled' }, + }), + ]) + expect(firstReview.data.review.updatedAt).toBeTruthy() + expect(Date.parse(firstReview.data.review.updatedAt)).toEqual(expect.any(Number)) + expect(secondReview.data.review.updatedAt).toBeTruthy() + expect(Date.parse(secondReview.data.review.updatedAt)).toEqual(expect.any(Number)) + expect(firstReview.data.review.updatedAt).not.toEqual(secondReview.data.review.updatedAt) + }) + }) + }) + + describe('review to disable', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await nonModerator.toJson() + }) + + it('non-moderator receives an authorization error', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + }) + + describe('moderate a comment', () => { + beforeEach(async () => { + const trollingComment = await factory.create('Comment', { + id: 'comment-id', + }) + const reportAgainstTrollingComment = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingComment.relateTo(nonModerator, 'filed', { + resourceId: 'comment-id', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + disableVariables = { + ...disableVariables, + resourceId: 'comment-id', + } + }) + + it('returns disabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Comment', id: 'comment-id' } } }, + errors: undefined, + }) + }) + + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Comment', id: 'comment-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + errors: undefined, + }) + }) + + it('updates .disabled on comment', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'Comment', id: 'comment-id', disabled: true } }, + }, + errors: undefined, + }) + }) + + it('can be closed with one review', async () => { + closeReportVariables = { + ...disableVariables, + closed: true, + } + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Comment', id: 'comment-id' }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, }) }) }) - describe('moderator', () => { + describe('moderate a post', () => { beforeEach(async () => { - authenticatedUser = await moderator.toJson() + const trollingPost = await factory.create('Post', { + id: 'post-id', + }) + const reportAgainstTrollingPost = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingPost.relateTo(nonModerator, 'filed', { + resourceId: 'post-id', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", + }), + reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'), + ]) + disableVariables = { + ...disableVariables, + resourceId: 'post-id', + } }) - describe('moderate a resource that is not a (Comment|Post|User) ', () => { - beforeEach(async () => { - variables = { - id: 'sample-tag-id', - } - await factory.create('Tag', { id: 'sample-tag-id' }) - }) - - it('returns null', async () => { - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: null }, - }) + it('returns disabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + }, + }, + errors: undefined, }) }) - describe('moderate a comment', () => { - beforeEach(async () => { - variables = {} - await factory.create('Comment', { - id: 'comment-id', - }) - }) - - it('returns disabled resource id', async () => { - variables = { id: 'comment-id' } - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'comment-id' }, - errors: undefined, - }) - }) - - it('changes .disabledBy', async () => { - variables = { id: 'comment-id' } - const before = { data: { Comment: [{ id: 'comment-id', disabledBy: null }] } } - const expected = { - data: { Comment: [{ id: 'comment-id', disabledBy: { id: 'moderator-id' } }] }, - } - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'comment-id' }, - }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) - }) - - it('updates .disabled on comment', async () => { - variables = { id: 'comment-id' } - const before = { data: { Comment: [{ id: 'comment-id', disabled: false }] } } - const expected = { data: { Comment: [{ id: 'comment-id', disabled: true }] } } - - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'comment-id' }, - }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + errors: undefined, }) }) - describe('moderate a post', () => { - beforeEach(async () => { - variables = {} - await factory.create('Post', { - id: 'sample-post-id', - }) + it('updates .disabled on post', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Post', id: 'post-id', disabled: true } } }, + errors: undefined, }) + }) - it('returns disabled resource id', async () => { - variables = { id: 'sample-post-id' } - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'sample-post-id' }, - }) + it('can be closed with one review', async () => { + closeReportVariables = { + ...disableVariables, + closed: true, + } + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, }) + }) + }) - it('changes .disabledBy', async () => { - variables = { id: 'sample-post-id' } - const before = { data: { Post: [{ id: 'sample-post-id', disabledBy: null }] } } - const expected = { - data: { Post: [{ id: 'sample-post-id', disabledBy: { id: 'moderator-id' } }] }, - } - - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'sample-post-id' }, - }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + describe('moderate a user', () => { + beforeEach(async () => { + const troll = await factory.create('User', { + id: 'user-id', }) + const reportAgainstTroll = await factory.create('Report') + await Promise.all([ + reportAgainstTroll.relateTo(nonModerator, 'filed', { + resourceId: 'user-id', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }), + reportAgainstTroll.relateTo(troll, 'belongsTo'), + ]) + disableVariables = { + ...disableVariables, + resourceId: 'user-id', + } + }) - it('updates .disabled on post', async () => { - const before = { data: { Post: [{ id: 'sample-post-id', disabled: false }] } } - const expected = { data: { Post: [{ id: 'sample-post-id', disabled: true }] } } - variables = { id: 'sample-post-id' } + it('returns disabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'User', id: 'user-id' } } }, + errors: undefined, + }) + }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'sample-post-id' }, - }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'User', id: 'user-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + errors: undefined, + }) + }) + + it('updates .disabled on user', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'User', id: 'user-id', disabled: true } } }, + errors: undefined, + }) + }) + + it('can be closed with one review', async () => { + closeReportVariables = { + ...disableVariables, + closed: true, + } + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'User', id: 'user-id' }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, }) }) }) }) }) - describe('enable', () => { + describe('review to re-enable after disabled', () => { describe('unautenticated user', () => { it('throws authorization error', async () => { - variables = { id: 'sample-post-id' } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + enableVariables = { + ...enableVariables, + resourceId: 'post-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ errors: [{ message: 'Not Authorised!' }], }) }) @@ -228,17 +481,17 @@ describe('moderate resources', () => { describe('authenticated user', () => { describe('non moderator', () => { beforeEach(async () => { - nonModerator = await factory.create('User', { - id: 'non-moderator', - name: 'Non Moderator', - email: 'non.moderator@example.org', - password: '1234', - }) authenticatedUser = await nonModerator.toJson() }) + it('throws authorization error', async () => { - variables = { id: 'sample-post-id' } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + enableVariables = { + ...enableVariables, + resourceId: 'post-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ errors: [{ message: 'Not Authorised!' }], }) }) @@ -248,101 +501,197 @@ describe('moderate resources', () => { beforeEach(async () => { authenticatedUser = await moderator.toJson() }) - describe('moderate a resource that is not a (Comment|Post|User) ', () => { - beforeEach(async () => { - await Promise.all([factory.create('Tag', { id: 'sample-tag-id' })]) - }) - - it('returns null', async () => { - await expect( - mutate({ mutation: enableMutation, variables: { id: 'sample-tag-id' } }), - ).resolves.toMatchObject({ - data: { enable: null }, - }) - }) - }) describe('moderate a comment', () => { beforeEach(async () => { - variables = { id: 'comment-id' } - await factory.create('Comment', { + const trollingComment = await factory.create('Comment', { id: 'comment-id', }) - await mutate({ mutation: disableMutation, variables }) + const reportAgainstTrollingComment = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingComment.relateTo(nonModerator, 'filed', { + resourceId: 'comment-id', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + await Promise.all([ + reportAgainstTrollingComment.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'comment-id', + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + enableVariables = { + ...enableVariables, + resourceId: 'comment-id', + } }) it('returns enabled resource id', async () => { - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'comment-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Comment', id: 'comment-id' } } }, }) }) - it('changes .disabledBy', async () => { - const expected = { - data: { Comment: [{ id: 'comment-id', disabledBy: null }] }, - errors: undefined, - } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'comment-id' }, - errors: undefined, + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Comment', id: 'comment-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) }) it('updates .disabled on comment', async () => { - const expected = { - data: { Comment: [{ id: 'comment-id', disabled: false }] }, - errors: undefined, - } - - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'comment-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'Comment', id: 'comment-id', disabled: false } }, + }, }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) }) }) describe('moderate a post', () => { beforeEach(async () => { - variables = { id: 'post-id' } - await factory.create('Post', { + const trollingPost = await factory.create('Post', { id: 'post-id', }) - await mutate({ mutation: disableMutation, variables }) + const reportAgainstTrollingPost = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingPost.relateTo(nonModerator, 'filed', { + resourceId: 'post-id', + reasonCategory: 'doxing', + reasonDescription: + "This shouldn't be shown to anybody else! It's my private thing!", + }), + reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'), + ]) + await Promise.all([ + reportAgainstTrollingPost.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'comment-id', + }), + trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + enableVariables = { + ...enableVariables, + resourceId: 'post-id', + } }) it('returns enabled resource id', async () => { - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'post-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Post', id: 'post-id' } } }, }) }) - it('changes .disabledBy', async () => { - const expected = { - data: { Post: [{ id: 'post-id', disabledBy: null }] }, - errors: undefined, - } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'post-id' }, - errors: undefined, + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) }) it('updates .disabled on post', async () => { - const expected = { - data: { Post: [{ id: 'post-id', disabled: false }] }, - errors: undefined, - } - - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'post-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'Post', id: 'post-id', disabled: false } }, + }, + }) + }) + }) + + describe('moderate a user', () => { + beforeEach(async () => { + const troll = await factory.create('User', { + id: 'user-id', + }) + const reportAgainstTroll = await factory.create('Report') + await Promise.all([ + reportAgainstTroll.relateTo(nonModerator, 'filed', { + resourceId: 'user-id', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }), + reportAgainstTroll.relateTo(troll, 'belongsTo'), + ]) + await Promise.all([ + reportAgainstTroll.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'comment-id', + }), + troll.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + enableVariables = { + ...enableVariables, + resourceId: 'user-id', + } + }) + + it('returns enabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'User', id: 'user-id' } } }, + }) + }) + + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'User', id: 'user-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + }) + }) + + it('updates .disabled on user', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'User', id: 'user-id', disabled: false } }, + }, }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) }) }) }) diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 9e6f5c91a..7f9c52e1e 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -18,7 +18,7 @@ export default { notifications: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() - let notifications, whereClause, orderByClause + let whereClause, orderByClause switch (args.read) { case true: @@ -42,27 +42,25 @@ export default { } const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : '' const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : '' - try { - const cypher = ` + const cypher = ` MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) ${whereClause} RETURN resource, notification, user ${orderByClause} ${offset} ${limit} ` + try { const result = await session.run(cypher, { id: currentUser.id }) - notifications = await result.records.map(transformReturnType) + return result.records.map(transformReturnType) } finally { session.close() } - return notifications }, }, Mutation: { markAsRead: async (parent, args, context, resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() - let notification try { const cypher = ` MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id}) @@ -71,11 +69,10 @@ export default { ` const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id }) const notifications = await result.records.map(transformReturnType) - notification = notifications[0] + return notifications[0] } finally { session.close() } - return notification }, }, NOTIFIED: { diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index 7c0d9e747..dfbfe8183 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -9,7 +9,6 @@ export default { return createPasswordReset({ driver, nonce, email }) }, resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { - const session = driver.session() const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) @@ -21,16 +20,20 @@ export default { SET u.encryptedPassword = $encryptedNewPassword RETURN pr ` - const transactionRes = await session.run(cypher, { - stillValid, - email, - nonce, - encryptedNewPassword, - }) - const [reset] = transactionRes.records.map(record => record.get('pr')) - const response = !!(reset && reset.properties.usedAt) - session.close() - return response + const session = driver.session() + try { + const transactionRes = await session.run(cypher, { + stillValid, + email, + nonce, + encryptedNewPassword, + }) + const [reset] = transactionRes.records.map(record => record.get('pr')) + const response = !!(reset && reset.properties.usedAt) + return response + } finally { + session.close() + } }, }, } diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 8b36b8c85..97aa6a020 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -15,10 +15,13 @@ let variables const getAllPasswordResets = async () => { const session = driver.session() - const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r') - const resets = transactionRes.records.map(record => record.get('r')) - session.close() - return resets + try { + const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r') + const resets = transactionRes.records.map(record => record.get('r')) + return resets + } finally { + session.close() + } } beforeEach(() => { diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index efb991cf8..b37a4abd5 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -5,6 +5,7 @@ import { getBlockedUsers, getBlockedByUsers } from './users.js' import { mergeWith, isArray, isEmpty } from 'lodash' import { UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' + const filterForBlockedUsers = async (params, context) => { if (!context.user) return params const [blockedUsers, blockedByUsers] = await Promise.all([ @@ -54,37 +55,41 @@ export default { return neo4jgraphql(object, params, context, resolveInfo) }, PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { postId, data } = params - const transactionRes = await session.run( - `MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() RETURN COUNT(DISTINCT emoted) as emotionsCount `, - { postId, data }, - ) - session.close() + { postId, data }, + ) - const [emotionsCount] = transactionRes.records.map(record => { - return record.get('emotionsCount').low - }) - - return emotionsCount + const [emotionsCount] = transactionRes.records.map(record => { + return record.get('emotionsCount').low + }) + return emotionsCount + } finally { + session.close() + } }, PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { postId } = params - const transactionRes = await session.run( - `MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) RETURN collect(emoted.emotion) as emotion`, - { userId: context.user.id, postId }, - ) + { userId: context.user.id, postId }, + ) - session.close() - - const [emotions] = transactionRes.records.map(record => { - return record.get('emotion') - }) - return emotions + const [emotions] = transactionRes.records.map(record => { + return record.get('emotion') + }) + return emotions + } finally { + session.close() + } }, }, Mutation: { @@ -93,8 +98,6 @@ export default { delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params.id = params.id || uuid() - let post - const createPostCypher = `CREATE (post:Post {params}) SET post.createdAt = toString(datetime()) SET post.updatedAt = toString(datetime()) @@ -113,7 +116,7 @@ export default { try { const transactionRes = await session.run(createPostCypher, createPostVariables) const posts = transactionRes.records.map(record => record.get('post').properties) - post = posts[0] + return posts[0] } catch (e) { if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') throw new UserInputError('Post with this slug already exists!') @@ -121,54 +124,55 @@ export default { } finally { session.close() } - return post }, UpdatePost: async (_parent, params, context, _resolveInfo) => { const { categoryIds } = params delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) - const session = context.driver.session() let updatePostCypher = `MATCH (post:Post {id: $params.id}) SET post += $params SET post.updatedAt = toString(datetime()) WITH post ` - if (categoryIds && categoryIds.length) { - const cypherDeletePreviousRelations = ` + const session = context.driver.session() + try { + if (categoryIds && categoryIds.length) { + const cypherDeletePreviousRelations = ` MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category) DELETE previousRelations RETURN post, category ` - await session.run(cypherDeletePreviousRelations, { params }) + await session.run(cypherDeletePreviousRelations, { params }) - updatePostCypher += ` + updatePostCypher += ` UNWIND $categoryIds AS categoryId MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category) WITH post ` + } + + updatePostCypher += `RETURN post` + const updatePostVariables = { categoryIds, params } + + const transactionRes = await session.run(updatePostCypher, updatePostVariables) + const [post] = transactionRes.records.map(record => { + return record.get('post').properties + }) + return post + } finally { + session.close() } - - updatePostCypher += `RETURN post` - const updatePostVariables = { categoryIds, params } - - const transactionRes = await session.run(updatePostCypher, updatePostVariables) - const [post] = transactionRes.records.map(record => { - return record.get('post').properties - }) - - session.close() - - return post }, DeletePost: async (object, args, context, resolveInfo) => { const session = context.driver.session() - // we cannot set slug to 'UNAVAILABE' because of unique constraints - const transactionRes = await session.run( - ` + try { + // we cannot set slug to 'UNAVAILABE' because of unique constraints + const transactionRes = await session.run( + ` MATCH (post:Post {id: $postId}) OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment) SET post.deleted = TRUE @@ -179,51 +183,60 @@ export default { REMOVE post.image RETURN post `, - { postId: args.id }, - ) - session.close() - const [post] = transactionRes.records.map(record => record.get('post').properties) - return post + { postId: args.id }, + ) + const [post] = transactionRes.records.map(record => record.get('post').properties) + return post + } finally { + session.close() + } }, AddPostEmotions: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { to, data } = params const { user } = context - const transactionRes = await session.run( - `MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id}) + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id}) MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo) RETURN userFrom, postTo, emotedRelation`, - { user, to, data }, - ) - session.close() - const [emoted] = transactionRes.records.map(record => { - return { - from: { ...record.get('userFrom').properties }, - to: { ...record.get('postTo').properties }, - ...record.get('emotedRelation').properties, - } - }) - return emoted + { user, to, data }, + ) + + const [emoted] = transactionRes.records.map(record => { + return { + from: { ...record.get('userFrom').properties }, + to: { ...record.get('postTo').properties }, + ...record.get('emotedRelation').properties, + } + }) + return emoted + } finally { + session.close() + } }, RemovePostEmotions: async (object, params, context, resolveInfo) => { - const session = context.driver.session() const { to, data } = params const { id: from } = context.user - const transactionRes = await session.run( - `MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) + const session = context.driver.session() + try { + const transactionRes = await session.run( + `MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) DELETE emotedRelation RETURN userFrom, postTo`, - { from, to, data }, - ) - session.close() - const [emoted] = transactionRes.records.map(record => { - return { - from: { ...record.get('userFrom').properties }, - to: { ...record.get('postTo').properties }, - emotion: data.emotion, - } - }) - return emoted + { from, to, data }, + ) + const [emoted] = transactionRes.records.map(record => { + return { + from: { ...record.get('userFrom').properties }, + to: { ...record.get('postTo').properties }, + emotion: data.emotion, + } + }) + return emoted + } finally { + session.close() + } }, pinPost: async (_parent, params, context, _resolveInfo) => { let pinnedPostWithNestedAttributes @@ -241,25 +254,25 @@ export default { ) return deletePreviousRelationsResponse.records.map(record => record.get('post').properties) }) - await writeTxResultPromise + try { + await writeTxResultPromise - writeTxResultPromise = session.writeTransaction(async transaction => { - const pinPostTransactionResponse = await transaction.run( - ` + writeTxResultPromise = session.writeTransaction(async transaction => { + const pinPostTransactionResponse = await transaction.run( + ` MATCH (user:User {id: $userId}) WHERE user.role = 'admin' MATCH (post:Post {id: $params.id}) MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post) SET post.pinned = true RETURN post, pinned.createdAt as pinnedAt `, - { userId, params }, - ) - return pinPostTransactionResponse.records.map(record => ({ - pinnedPost: record.get('post').properties, - pinnedAt: record.get('pinnedAt'), - })) - }) - try { + { userId, params }, + ) + return pinPostTransactionResponse.records.map(record => ({ + pinnedPost: record.get('post').properties, + pinnedAt: record.get('pinnedAt'), + })) + }) const [transactionResult] = await writeTxResultPromise const { pinnedPost, pinnedAt } = transactionResult pinnedPostWithNestedAttributes = { @@ -314,7 +327,6 @@ export default { }, hasOne: { author: '<-[:WROTE]-(related:User)', - disabledBy: '<-[:DISABLED]-(related:User)', pinnedBy: '<-[:PINNED]-(related:User)', }, count: { diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index d6a97191d..98475b182 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -316,53 +316,6 @@ describe('CreatePost', () => { ) }) }) - - describe('categories', () => { - describe('null', () => { - beforeEach(() => { - variables = { ...variables, categoryIds: null } - }) - it('throws UserInputError', async () => { - const { - errors: [error], - } = await mutate({ mutation: createPostMutation, variables }) - expect(error).toHaveProperty( - 'message', - 'You cannot save a post without at least one category or more than three', - ) - }) - }) - - describe('empty', () => { - beforeEach(() => { - variables = { ...variables, categoryIds: [] } - }) - it('throws UserInputError', async () => { - const { - errors: [error], - } = await mutate({ mutation: createPostMutation, variables }) - expect(error).toHaveProperty( - 'message', - 'You cannot save a post without at least one category or more than three', - ) - }) - }) - - describe('more than 3 items', () => { - beforeEach(() => { - variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] } - }) - it('throws UserInputError', async () => { - const { - errors: [error], - } = await mutate({ mutation: createPostMutation, variables }) - expect(error).toHaveProperty( - 'message', - 'You cannot save a post without at least one category or more than three', - ) - }) - }) - }) }) }) @@ -493,74 +446,6 @@ describe('UpdatePost', () => { expected, ) }) - - describe('more than 3 categories', () => { - beforeEach(() => { - variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] } - }) - - it('allows a maximum of three category for a successful update', async () => { - const { - errors: [error], - } = await mutate({ mutation: updatePostMutation, variables }) - expect(error).toHaveProperty( - 'message', - 'You cannot save a post without at least one category or more than three', - ) - }) - }) - - describe('post created without categories somehow', () => { - let owner - - beforeEach(async () => { - const postSomehowCreated = await neode.create('Post', { - id: 'how-was-this-created', - }) - owner = await neode.create('User', { - id: 'author-of-post-without-category', - name: 'Hacker', - slug: 'hacker', - email: 'hacker@example.org', - password: '1234', - }) - await postSomehowCreated.relateTo(owner, 'author') - authenticatedUser = await owner.toJson() - variables = { ...variables, id: 'how-was-this-created' } - }) - - it('throws an error if categoryIds is not an array', async () => { - const { - errors: [error], - } = await mutate({ - mutation: updatePostMutation, - variables: { - ...variables, - categoryIds: null, - }, - }) - expect(error).toHaveProperty( - 'message', - 'You cannot save a post without at least one category or more than three', - ) - }) - - it('requires at least one category for successful update', async () => { - const { - errors: [error], - } = await mutate({ - mutation: updatePostMutation, - variables: { - ...variables, - categoryIds: [], - }, - }) - expect(error).toHaveProperty( - 'message', - 'You cannot save a post without at least one category or more than three', - ) - }) - }) }) }) diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index d425357c3..206c8db74 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -4,31 +4,12 @@ import fileUpload from './fileUpload' import encryptPassword from '../../helpers/encryptPassword' import generateNonce from './helpers/generateNonce' import existingEmailAddress from './helpers/existingEmailAddress' -import { normalizeEmail } from 'validator' +import normalizeEmail from './helpers/normalizeEmail' const instance = neode() export default { Mutation: { - CreateInvitationCode: async (_parent, args, context, _resolveInfo) => { - args.token = generateNonce() - const { - user: { id: userId }, - } = context - let response - try { - const [user, invitationCode] = await Promise.all([ - instance.find('User', userId), - instance.create('InvitationCode', args), - ]) - await invitationCode.relateTo(user, 'generatedBy') - response = invitationCode.toJson() - response.generatedBy = user.toJson() - } catch (e) { - throw new UserInputError(e) - } - return response - }, Signup: async (_parent, args, context) => { args.nonce = generateNonce() args.email = normalizeEmail(args.email) @@ -41,35 +22,6 @@ export default { throw new UserInputError(e.message) } }, - SignupByInvitation: async (_parent, args, context) => { - const { token } = args - args.nonce = generateNonce() - args.email = normalizeEmail(args.email) - let emailAddress = await existingEmailAddress({ args, context }) - if (emailAddress) return emailAddress - try { - const result = await instance.cypher( - ` - MATCH (invitationCode:InvitationCode {token:{token}}) - WHERE NOT (invitationCode)-[:ACTIVATED]->() - RETURN invitationCode - `, - { token }, - ) - const validInvitationCode = instance.hydrateFirst( - result, - 'invitationCode', - instance.model('InvitationCode'), - ) - if (!validInvitationCode) - throw new UserInputError('Invitation code already used or does not exist.') - emailAddress = await instance.create('EmailAddress', args) - await validInvitationCode.relateTo(emailAddress, 'activated') - return emailAddress.toJson() - } catch (e) { - throw new UserInputError(e) - } - }, SignupVerification: async (_parent, args) => { const { termsAndConditionsAgreedVersion } = args const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 7d53b63c9..35b16b9bb 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -9,7 +9,6 @@ const neode = getNeode() let mutate let authenticatedUser -let user let variables const driver = getDriver() @@ -34,243 +33,6 @@ afterEach(async () => { await factory.cleanDatabase() }) -describe('CreateInvitationCode', () => { - const mutation = gql` - mutation { - CreateInvitationCode { - token - } - } - ` - - describe('unauthenticated', () => { - beforeEach(() => { - authenticatedUser = null - }) - - it('throws Authorization error', async () => { - await expect(mutate({ mutation })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], - }) - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - user = await factory.create('User', { - id: 'i123', - name: 'Inviter', - email: 'inviter@example.org', - password: '1234', - termsAndConditionsAgreedVersion: null, - }) - authenticatedUser = await user.toJson() - }) - - it('resolves', async () => { - await expect(mutate({ mutation })).resolves.toMatchObject({ - data: { CreateInvitationCode: { token: expect.any(String) } }, - }) - }) - - it('creates an InvitationCode with a `createdAt` attribute', async () => { - await mutate({ mutation }) - const codes = await neode.all('InvitationCode') - const invitation = await codes.first().toJson() - expect(invitation.createdAt).toBeTruthy() - expect(Date.parse(invitation.createdAt)).toEqual(expect.any(Number)) - }) - - it('relates inviting User to InvitationCode', async () => { - await mutate({ mutation }) - const result = await neode.cypher( - 'MATCH(code:InvitationCode)<-[:GENERATED]-(user:User) RETURN user', - ) - const inviter = neode.hydrateFirst(result, 'user', neode.model('User')) - await expect(inviter.toJson()).resolves.toEqual(expect.objectContaining({ name: 'Inviter' })) - }) - - describe('who has invited a lot of users already', () => { - beforeEach(async () => { - await Promise.all([mutate({ mutation }), mutate({ mutation }), mutate({ mutation })]) - }) - - describe('as ordinary `user`', () => { - it('throws `Not Authorised` because of maximum number of invitations', async () => { - await expect(mutate({ mutation })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], - }) - }) - - it('creates no additional invitation codes', async () => { - await mutate({ mutation }) - const invitationCodes = await neode.all('InvitationCode') - await expect(invitationCodes.toJson()).resolves.toHaveLength(3) - }) - }) - - describe('as a strong donator', () => { - beforeEach(() => { - // What is the setup? - }) - - it.todo('can invite more people') - // it('can invite more people', async () => { - // await action() - // const invitationQuery = `{ User { createdAt } }` - // const { User: users } = await client.request(invitationQuery ) - // expect(users).toHaveLength(3 + 1 + 1) - // }) - }) - }) - }) -}) - -describe('SignupByInvitation', () => { - const mutation = gql` - mutation($email: String!, $token: String!) { - SignupByInvitation(email: $email, token: $token) { - email - } - } - ` - - describe('with valid email but invalid InvitationCode', () => { - beforeEach(() => { - variables = { - ...variables, - email: 'any-email@example.org', - token: 'wut?', - } - }) - - it('throws UserInputError', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'UserInputError: Invitation code already used or does not exist.' }], - }) - }) - - describe('with valid InvitationCode', () => { - beforeEach(async () => { - const inviter = await factory.create('User', { - name: 'Inviter', - email: 'inviter@example.org', - password: '1234', - }) - authenticatedUser = await inviter.toJson() - const invitationMutation = gql` - mutation { - CreateInvitationCode { - token - } - } - ` - const { - data: { - CreateInvitationCode: { token }, - }, - } = await mutate({ mutation: invitationMutation }) - authenticatedUser = null - variables = { - ...variables, - token, - } - }) - - describe('given an invalid email', () => { - beforeEach(() => { - variables = { ...variables, email: 'someuser' } - }) - - it('throws `email is not a valid email`', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: expect.stringContaining('"email" must be a valid email') }], - }) - }) - - it('creates no additional EmailAddress node', async () => { - let emailAddresses = await neode.all('EmailAddress') - emailAddresses = await emailAddresses.toJson() - expect(emailAddresses).toHaveLength(1) - await mutate({ mutation, variables }) - emailAddresses = await neode.all('EmailAddress') - emailAddresses = await emailAddresses.toJson() - expect(emailAddresses).toHaveLength(1) - }) - }) - - describe('given a valid email', () => { - beforeEach(() => { - variables = { ...variables, email: 'someUser@example.org' } - }) - - it('resolves', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - data: { SignupByInvitation: { email: 'someuser@example.org' } }, - }) - }) - - describe('creates a EmailAddress node', () => { - it('with a `createdAt` attribute', async () => { - await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.createdAt).toBeTruthy() - expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) - }) - - it('with a cryptographic `nonce`', async () => { - await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) - }) - - it('connects inviter through invitation code', async () => { - await mutate({ mutation, variables }) - const result = await neode.cypher( - 'MATCH(inviter:User)-[:GENERATED]->(:InvitationCode)-[:ACTIVATED]->(email:EmailAddress {email: {email}}) RETURN inviter', - { email: 'someuser@example.org' }, - ) - const inviter = neode.hydrateFirst(result, 'inviter', neode.model('User')) - await expect(inviter.toJson()).resolves.toEqual( - expect.objectContaining({ name: 'Inviter' }), - ) - }) - - describe('using the same InvitationCode twice', () => { - it('rejects because codes can be used only once', async () => { - await mutate({ mutation, variables }) - variables = { ...variables, email: 'yetanotheremail@example.org' } - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [ - { message: 'UserInputError: Invitation code already used or does not exist.' }, - ], - }) - }) - }) - - describe('if a user account with the given email already exists', () => { - beforeEach(async () => { - await factory.create('User', { email: 'someuser@example.org' }) - }) - - it('throws unique violation error', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'A user account with this email already exists.' }], - }) - }) - }) - - describe('if the EmailAddress already exists but without user account', () => { - it.todo('shall we re-send the registration email?') - }) - }) - }) - }) - }) -}) - describe('Signup', () => { const mutation = gql` mutation($email: String!) { diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 083c94362..fc93229ae 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -1,18 +1,32 @@ +const transformReturnType = record => { + return { + ...record.get('report').properties, + resource: { + __typename: record.get('type'), + ...record.get('resource').properties, + }, + } +} + export default { Mutation: { - report: async (_parent, params, context, _resolveInfo) => { + fileReport: async (_parent, params, context, _resolveInfo) => { let createdRelationshipWithNestedAttributes const { resourceId, reasonCategory, reasonDescription } = params const { driver, user } = context const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async txc => { - const reportRelationshipTransactionResponse = await txc.run( + const reportWriteTxResultPromise = session.writeTransaction(async txc => { + const reportTransactionResponse = await txc.run( ` MATCH (submitter:User {id: $submitterId}) MATCH (resource {id: $resourceId}) - WHERE resource:User OR resource:Comment OR resource:Post - CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) - RETURN report, submitter, resource, labels(resource)[0] as type + WHERE resource:User OR resource:Post OR resource:Comment + MERGE (resource)<-[:BELONGS_TO]-(report:Report {closed: false}) + ON CREATE SET report.id = randomUUID(), report.createdAt = $createdAt, report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.disable = resource.disabled, report.closed = false + WITH submitter, resource, report + CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) + + RETURN report, resource, labels(resource)[0] AS type `, { resourceId, @@ -22,36 +36,12 @@ export default { reasonDescription, }, ) - return reportRelationshipTransactionResponse.records.map(record => ({ - report: record.get('report'), - submitter: record.get('submitter'), - resource: record.get('resource').properties, - type: record.get('type'), - })) + return reportTransactionResponse.records.map(transformReturnType) }) try { - const txResult = await writeTxResultPromise + const txResult = await reportWriteTxResultPromise if (!txResult[0]) return null - const { report, submitter, resource, type } = txResult[0] - createdRelationshipWithNestedAttributes = { - ...report.properties, - post: null, - comment: null, - user: null, - submitter: submitter.properties, - type, - } - switch (type) { - case 'Post': - createdRelationshipWithNestedAttributes.post = resource - break - case 'Comment': - createdRelationshipWithNestedAttributes.comment = resource - break - case 'User': - createdRelationshipWithNestedAttributes.user = resource - break - } + createdRelationshipWithNestedAttributes = txResult[0] } finally { session.close() } @@ -62,8 +52,7 @@ export default { reports: async (_parent, params, context, _resolveInfo) => { const { driver } = context const session = driver.session() - let response - let orderByClause + let reports, orderByClause switch (params.orderBy) { case 'createdAt_asc': orderByClause = 'ORDER BY report.createdAt ASC' @@ -74,55 +63,97 @@ export default { default: orderByClause = '' } - try { - const cypher = ` - MATCH (submitter:User)-[report:REPORTED]->(resource) - WHERE resource:User OR resource:Comment OR resource:Post - RETURN report, submitter, resource, labels(resource)[0] as type + const reportReadTxPromise = session.readTransaction(async tx => { + const allReportsTransactionResponse = await tx.run( + ` + MATCH (submitter:User)-[filed:FILED]->(report:Report)-[:BELONGS_TO]->(resource) + WHERE resource:User OR resource:Post OR resource:Comment + RETURN DISTINCT report, resource, labels(resource)[0] as type ${orderByClause} - ` - const result = await session.run(cypher, {}) - const dbResponse = result.records.map(r => { - return { - report: r.get('report'), - submitter: r.get('submitter'), - resource: r.get('resource'), - type: r.get('type'), + `, + {}, + ) + return allReportsTransactionResponse.records.map(transformReturnType) + }) + try { + const txResult = await reportReadTxPromise + if (!txResult[0]) return null + reports = txResult + } finally { + session.close() + } + return reports + }, + }, + Report: { + filed: async (parent, _params, context, _resolveInfo) => { + if (typeof parent.filed !== 'undefined') return parent.filed + const session = context.driver.session() + const { id } = parent + let filed + const readTxPromise = session.readTransaction(async tx => { + const allReportsTransactionResponse = await tx.run( + ` + MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) + RETURN filed, submitter + `, + { id }, + ) + return allReportsTransactionResponse.records.map(record => ({ + submitter: record.get('submitter').properties, + filed: record.get('filed').properties, + })) + }) + try { + const txResult = await readTxPromise + if (!txResult[0]) return null + filed = txResult.map(reportedRecord => { + const { submitter, filed } = reportedRecord + const relationshipWithNestedAttributes = { + ...filed, + submitter, } - }) - if (!dbResponse) return null - - response = [] - dbResponse.forEach(ele => { - const { report, submitter, resource, type } = ele - - const responseEle = { - ...report.properties, - post: null, - comment: null, - user: null, - submitter: submitter.properties, - type, - } - - switch (type) { - case 'Post': - responseEle.post = resource.properties - break - case 'Comment': - responseEle.comment = resource.properties - break - case 'User': - responseEle.user = resource.properties - break - } - response.push(responseEle) + return relationshipWithNestedAttributes }) } finally { session.close() } - - return response + return filed + }, + reviewed: async (parent, _params, context, _resolveInfo) => { + if (typeof parent.reviewed !== 'undefined') return parent.reviewed + const session = context.driver.session() + const { id } = parent + let reviewed + const readTxPromise = session.readTransaction(async tx => { + const allReportsTransactionResponse = await tx.run( + ` + MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) + RETURN moderator, review + ORDER BY report.updatedAt DESC, review.updatedAt DESC + `, + { id }, + ) + return allReportsTransactionResponse.records.map(record => ({ + review: record.get('review').properties, + moderator: record.get('moderator').properties, + })) + }) + try { + const txResult = await readTxPromise + if (!txResult[0]) return null + reviewed = txResult.map(reportedRecord => { + const { review, moderator } = reportedRecord + const relationshipWithNestedAttributes = { + ...review, + moderator, + } + return relationshipWithNestedAttributes + }) + } finally { + session.close() + } + return reviewed }, }, } diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 2ecd1f20d..c0a9d3afb 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -8,31 +8,41 @@ const factory = Factory() const instance = getNeode() const driver = getDriver() -describe('report resources', () => { - let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser +describe('file a report on a resource', () => { + let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser const categoryIds = ['cat9'] const reportMutation = gql` mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report( + fileReport( resourceId: $resourceId reasonCategory: $reasonCategory reasonDescription: $reasonDescription ) { + id createdAt - reasonCategory - reasonDescription - type - submitter { - email + updatedAt + disable + closed + rule + resource { + __typename + ... on User { + name + } + ... on Post { + title + } + ... on Comment { + content + } } - user { - name - } - post { - title - } - comment { - content + filed { + submitter { + id + } + createdAt + reasonCategory + reasonDescription } } } @@ -67,7 +77,7 @@ describe('report resources', () => { it('throws authorization error', async () => { authenticatedUser = null await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({ - data: { report: null }, + data: { fileReport: null }, errors: [{ message: 'Not Authorised!' }], }) }) @@ -81,6 +91,12 @@ describe('report resources', () => { email: 'test@example.org', password: '1234', }) + otherReportingUser = await factory.create('User', { + id: 'other-reporting-user-id', + role: 'user', + email: 'reporting@example.org', + password: '1234', + }) await factory.create('User', { id: 'abusive-user-id', role: 'user', @@ -99,15 +115,15 @@ describe('report resources', () => { describe('invalid resource id', () => { it('returns null', async () => { await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({ - data: { report: null }, + data: { fileReport: null }, errors: undefined, }) }) }) describe('valid resource', () => { - describe('reported resource is a user', () => { - it('returns type "User"', async () => { + describe('creates report', () => { + it('which belongs to resource', async () => { await expect( mutate({ mutation: reportMutation, @@ -115,15 +131,28 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - type: 'User', + fileReport: { + id: expect.any(String), }, }, errors: undefined, }) }) - it('returns resource in user attribute', async () => { + it('creates only one report for multiple reports on the same resource', async () => { + const firstReport = await mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }) + authenticatedUser = await otherReportingUser.toJson() + const secondReport = await mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }) + expect(firstReport.data.fileReport.id).toEqual(secondReport.data.fileReport.id) + }) + + it('returns the rule for how the report was decided', async () => { await expect( mutate({ mutation: reportMutation, @@ -131,8 +160,46 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - user: { + fileReport: { + rule: 'latestReviewUpdatedAtRules', + }, + }, + errors: undefined, + }) + }) + it.todo('creates multiple filed reports') + }) + + describe('reported resource is a user', () => { + it('returns __typename "User"', async () => { + await expect( + mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }), + ).resolves.toMatchObject({ + data: { + fileReport: { + resource: { + __typename: 'User', + }, + }, + }, + errors: undefined, + }) + }) + + it('returns user attribute info', async () => { + await expect( + mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }), + ).resolves.toMatchObject({ + data: { + fileReport: { + resource: { + __typename: 'User', name: 'abusive-user', }, }, @@ -149,10 +216,14 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - submitter: { - email: 'test@example.org', - }, + fileReport: { + filed: [ + { + submitter: { + id: 'current-user-id', + }, + }, + ], }, }, errors: undefined, @@ -167,7 +238,7 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { + fileReport: { createdAt: expect.any(String), }, }, @@ -187,8 +258,12 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - reasonCategory: 'criminal_behavior_violation_german_law', + fileReport: { + filed: [ + { + reasonCategory: 'criminal_behavior_violation_german_law', + }, + ], }, }, errors: undefined, @@ -228,15 +303,19 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - reasonDescription: 'My reason!', + fileReport: { + filed: [ + { + reasonDescription: 'My reason!', + }, + ], }, }, errors: undefined, }) }) - it('sanitize the reason description', async () => { + it('sanitizes the reason description', async () => { await expect( mutate({ mutation: reportMutation, @@ -248,8 +327,12 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - reasonDescription: 'My reason !', + fileReport: { + filed: [ + { + reasonDescription: 'My reason !', + }, + ], }, }, errors: undefined, @@ -278,8 +361,10 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - type: 'Post', + fileReport: { + resource: { + __typename: 'Post', + }, }, }, errors: undefined, @@ -297,8 +382,9 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - post: { + fileReport: { + resource: { + __typename: 'Post', title: 'This is a post that is going to be reported', }, }, @@ -306,25 +392,6 @@ describe('report resources', () => { errors: undefined, }) }) - - it('returns null in user attribute', async () => { - await expect( - mutate({ - mutation: reportMutation, - variables: { - ...variables, - resourceId: 'post-to-report-id', - }, - }), - ).resolves.toMatchObject({ - data: { - report: { - user: null, - }, - }, - errors: undefined, - }) - }) }) describe('reported resource is a comment', () => { @@ -356,8 +423,10 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - type: 'Comment', + fileReport: { + resource: { + __typename: 'Comment', + }, }, }, errors: undefined, @@ -375,8 +444,9 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - comment: { + fileReport: { + resource: { + __typename: 'Comment', content: 'Post comment to be reported.', }, }, @@ -403,7 +473,7 @@ describe('report resources', () => { }, }), ).resolves.toMatchObject({ - data: { report: null }, + data: { fileReport: null }, errors: undefined, }) }) @@ -411,25 +481,35 @@ describe('report resources', () => { }) }) }) + describe('query for reported resource', () => { const reportsQuery = gql` query { reports(orderBy: createdAt_desc) { + id createdAt - reasonCategory - reasonDescription - submitter { - id + updatedAt + disable + closed + resource { + __typename + ... on User { + id + } + ... on Post { + id + } + ... on Comment { + id + } } - type - user { - id - } - post { - id - } - comment { - id + filed { + submitter { + id + } + createdAt + reasonCategory + reasonDescription } } } @@ -437,7 +517,6 @@ describe('report resources', () => { beforeEach(async () => { authenticatedUser = null - moderator = await factory.create('User', { id: 'moderator-1', role: 'moderator', @@ -518,6 +597,7 @@ describe('report resources', () => { ]) authenticatedUser = null }) + describe('unauthenticated', () => { it('throws authorization error', async () => { authenticatedUser = null @@ -527,6 +607,7 @@ describe('report resources', () => { }) }) }) + describe('authenticated', () => { it('role "user" gets no reports', async () => { authenticatedUser = await currentUser.toJson() @@ -538,49 +619,69 @@ describe('report resources', () => { it('role "moderator" gets reports', async () => { const expected = { - // to check 'orderBy: createdAt_desc' is not possible here, because 'createdAt' does not differ reports: expect.arrayContaining([ expect.objectContaining({ + id: expect.any(String), createdAt: expect.any(String), - reasonCategory: 'doxing', - reasonDescription: 'This user is harassing me with bigoted remarks', - submitter: expect.objectContaining({ - id: 'current-user-id', - }), - type: 'User', - user: expect.objectContaining({ + updatedAt: expect.any(String), + disable: false, + closed: false, + resource: { + __typename: 'User', id: 'abusive-user-1', - }), - post: null, - comment: null, + }, + filed: expect.arrayContaining([ + expect.objectContaining({ + submitter: expect.objectContaining({ + id: 'current-user-id', + }), + createdAt: expect.any(String), + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', + }), + ]), }), expect.objectContaining({ + id: expect.any(String), createdAt: expect.any(String), - reasonCategory: 'other', - reasonDescription: 'This comment is bigoted', - submitter: expect.objectContaining({ - id: 'current-user-id', - }), - type: 'Post', - user: null, - post: expect.objectContaining({ + updatedAt: expect.any(String), + disable: false, + closed: false, + resource: { + __typename: 'Post', id: 'abusive-post-1', - }), - comment: null, + }, + filed: expect.arrayContaining([ + expect.objectContaining({ + submitter: expect.objectContaining({ + id: 'current-user-id', + }), + createdAt: expect.any(String), + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }), + ]), }), expect.objectContaining({ + id: expect.any(String), createdAt: expect.any(String), - reasonCategory: 'discrimination_etc', - reasonDescription: 'This post is bigoted', - submitter: expect.objectContaining({ - id: 'current-user-id', - }), - type: 'Comment', - user: null, - post: null, - comment: expect.objectContaining({ + updatedAt: expect.any(String), + disable: false, + closed: false, + resource: { + __typename: 'Comment', id: 'abusive-comment-1', - }), + }, + filed: expect.arrayContaining([ + expect.objectContaining({ + submitter: expect.objectContaining({ + id: 'current-user-id', + }), + createdAt: expect.any(String), + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + }), + ]), }), ]), } diff --git a/backend/src/schema/resolvers/shout.js b/backend/src/schema/resolvers/shout.js index 05de9b103..ada1172a4 100644 --- a/backend/src/schema/resolvers/shout.js +++ b/backend/src/schema/resolvers/shout.js @@ -4,48 +4,51 @@ export default { const { id, type } = params const session = context.driver.session() - const transactionRes = await session.run( - `MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) + try { + const transactionRes = await session.run( + `MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) WHERE $type IN labels(node) AND NOT userWritten.id = $userId MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node) RETURN COUNT(relation) > 0 as isShouted`, - { - id, - type, - userId: context.user.id, - }, - ) + { + id, + type, + userId: context.user.id, + }, + ) - const [isShouted] = transactionRes.records.map(record => { - return record.get('isShouted') - }) + const [isShouted] = transactionRes.records.map(record => { + return record.get('isShouted') + }) - session.close() - - return isShouted + return isShouted + } finally { + session.close() + } }, unshout: async (_object, params, context, _resolveInfo) => { const { id, type } = params const session = context.driver.session() - - const transactionRes = await session.run( - `MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id}) + try { + const transactionRes = await session.run( + `MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id}) WHERE $type IN labels(node) DELETE relation RETURN COUNT(relation) > 0 as isShouted`, - { - id, - type, - userId: context.user.id, - }, - ) - const [isShouted] = transactionRes.records.map(record => { - return record.get('isShouted') - }) - session.close() - - return isShouted + { + id, + type, + userId: context.user.id, + }, + ) + const [isShouted] = transactionRes.records.map(record => { + return record.get('isShouted') + }) + return isShouted + } finally { + session.close() + } }, }, } diff --git a/backend/src/schema/resolvers/statistics.js b/backend/src/schema/resolvers/statistics.js index 7b06f8705..07b9e4cea 100644 --- a/backend/src/schema/resolvers/statistics.js +++ b/backend/src/schema/resolvers/statistics.js @@ -1,6 +1,6 @@ export default { Query: { - statistics: async (parent, args, { driver, user }) => { + statistics: async (_parent, _args, { driver }) => { const session = driver.session() const response = {} try { @@ -33,10 +33,10 @@ export default { * Note: invites count is calculated this way because invitation codes are not in use yet */ response.countInvites = response.countEmails - response.countUsers + return response } finally { session.close() } - return response }, }, } diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js new file mode 100644 index 000000000..7ffa8ebd0 --- /dev/null +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -0,0 +1,140 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory from '../../seed/factories' +import { gql } from '../../helpers/jest' +import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' + +let query, authenticatedUser +const factory = Factory() +const instance = getNeode() +const driver = getDriver() + +const statisticsQuery = gql` + query { + statistics { + countUsers + countPosts + countComments + countNotifications + countInvites + countFollows + countShouts + } + } +` +beforeAll(() => { + authenticatedUser = undefined + const { server } = createServer({ + context: () => { + return { + driver, + neode: instance, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('statistics', () => { + describe('countUsers', () => { + beforeEach(async () => { + await Promise.all( + [...Array(6).keys()].map(() => { + return factory.create('User') + }), + ) + }) + + it('returns the count of all users', async () => { + await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ + data: { statistics: { countUsers: 6 } }, + errors: undefined, + }) + }) + }) + + describe('countPosts', () => { + beforeEach(async () => { + await Promise.all( + [...Array(3).keys()].map(() => { + return factory.create('Post') + }), + ) + }) + + it('returns the count of all posts', async () => { + await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ + data: { statistics: { countPosts: 3 } }, + errors: undefined, + }) + }) + }) + + describe('countComments', () => { + beforeEach(async () => { + await Promise.all( + [...Array(2).keys()].map(() => { + return factory.create('Comment') + }), + ) + }) + + it('returns the count of all comments', async () => { + await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ + data: { statistics: { countComments: 2 } }, + errors: undefined, + }) + }) + }) + + describe('countFollows', () => { + let users + beforeEach(async () => { + users = await Promise.all( + [...Array(2).keys()].map(() => { + return factory.create('User') + }), + ) + await users[0].relateTo(users[1], 'following') + }) + + it('returns the count of all follows', async () => { + await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ + data: { statistics: { countFollows: 1 } }, + errors: undefined, + }) + }) + }) + + describe('countShouts', () => { + let users, posts + beforeEach(async () => { + users = await Promise.all( + [...Array(2).keys()].map(() => { + return factory.create('User') + }), + ) + posts = await Promise.all( + [...Array(3).keys()].map(() => { + return factory.create('Post') + }), + ) + await Promise.all([ + users[0].relateTo(posts[1], 'shouted'), + users[1].relateTo(posts[0], 'shouted'), + ]) + }) + + it('returns the count of all shouts', async () => { + await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ + data: { statistics: { countShouts: 2 } }, + errors: undefined, + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index b7584dd68..4c4c3fc90 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -2,7 +2,7 @@ import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' import { neode } from '../../bootstrap/neo4j' -import { normalizeEmail } from 'validator' +import normalizeEmail from './helpers/normalizeEmail' const instance = neode() @@ -24,29 +24,32 @@ export default { // } email = normalizeEmail(email) const session = driver.session() - const result = await session.run( - ` + try { + const result = await session.run( + ` MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail}) RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1 `, - { userEmail: email }, - ) - session.close() - const [currentUser] = await result.records.map(record => { - return record.get('user') - }) + { userEmail: email }, + ) + const [currentUser] = await result.records.map(record => { + return record.get('user') + }) - if ( - currentUser && - (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && - !currentUser.disabled - ) { - delete currentUser.encryptedPassword - return encode(currentUser) - } else if (currentUser && currentUser.disabled) { - throw new AuthenticationError('Your account has been disabled.') - } else { - throw new AuthenticationError('Incorrect email address or password.') + if ( + currentUser && + (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && + !currentUser.disabled + ) { + delete currentUser.encryptedPassword + return encode(currentUser) + } else if (currentUser && currentUser.disabled) { + throw new AuthenticationError('Your account has been disabled.') + } else { + throw new AuthenticationError('Incorrect email address or password.') + } + } finally { + session.close() } }, changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index df8454ebb..e67b90c8d 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -9,25 +9,25 @@ import { neode as getNeode } from '../../bootstrap/neo4j' const factory = Factory() const neode = getNeode() -let query -let mutate -let variables -let req -let user +let query, mutate, variables, req, user const disable = async id => { - await factory.create('User', { id: 'u2', role: 'moderator' }) - const moderatorBearerToken = encode({ id: 'u2' }) - req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } } - await mutate({ - mutation: gql` - mutation($id: ID!) { - disable(id: $id) - } - `, - variables: { id }, - }) - req = { headers: {} } + const moderator = await factory.create('User', { id: 'u2', role: 'moderator' }) + const user = await neode.find('User', id) + const reportAgainstUser = await factory.create('Report') + await Promise.all([ + reportAgainstUser.relateTo(moderator, 'filed', { + resourceId: id, + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }), + reportAgainstUser.relateTo(user, 'belongsTo'), + ]) + const disableVariables = { resourceId: user.id, disable: true, closed: false } + await Promise.all([ + reportAgainstUser.relateTo(moderator, 'reviewed', disableVariables), + user.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) } beforeEach(() => { diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 02ed8dbac..c44e3f44b 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -212,7 +212,6 @@ export default { }, hasOne: { invitedBy: '<-[:INVITED]-(related:User)', - disabledBy: '<-[:DISABLED]-(related:User)', location: '-[:IS_IN]->(related:Location)', }, hasMany: { diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 27fd2206c..35998b935 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -24,8 +24,6 @@ type Mutation { changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! - disable(id: ID!): ID - enable(id: ID!): ID # Shout the given Type and ID shout(id: ID!, type: ShoutTypeEnum): Boolean! # Unshout the given Type and ID diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql index ba9d7a3fc..cf53df41d 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/schema/types/type/Comment.gql @@ -47,7 +47,6 @@ type Comment { updatedAt: String deleted: Boolean disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") } type Query { diff --git a/backend/src/schema/types/type/FILED.gql b/backend/src/schema/types/type/FILED.gql new file mode 100644 index 000000000..955af0bb8 --- /dev/null +++ b/backend/src/schema/types/type/FILED.gql @@ -0,0 +1,23 @@ +type FILED { + createdAt: String! + reasonCategory: ReasonCategory! + reasonDescription: String! + submitter: User +} + +# this list equals the strings of an array in file "webapp/constants/modals.js" +enum ReasonCategory { + other + discrimination_etc + pornographic_content_links + glorific_trivia_of_cruel_inhuman_acts + doxing + intentional_intimidation_stalking_persecution + advert_products_services_commercial + criminal_behavior_violation_german_law +} + +enum ReportOrdering { + createdAt_asc + createdAt_desc +} diff --git a/backend/src/schema/types/type/InvitationCode.gql b/backend/src/schema/types/type/InvitationCode.gql deleted file mode 100644 index 61ce0f689..000000000 --- a/backend/src/schema/types/type/InvitationCode.gql +++ /dev/null @@ -1,10 +0,0 @@ -type InvitationCode { - id: ID! - token: String - generatedBy: User @relation(name: "GENERATED", direction: "IN") - createdAt: String -} - -type Mutation { - CreateInvitationCode: InvitationCode -} diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 5557cbd54..af91460f7 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -26,7 +26,7 @@ enum NotificationReason { type Query { notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED] } - + type Mutation { markAsRead(id: ID!): NOTIFIED } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 24c00e3b6..0f1817971 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -114,7 +114,7 @@ type Post { objectId: String author: User @relation(name: "WROTE", direction: "IN") title: String! - slug: String + slug: String! content: String! contentExcerpt: String image: String @@ -124,7 +124,6 @@ type Post { deleted: Boolean disabled: Boolean pinned: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") createdAt: String updatedAt: String language: String diff --git a/backend/src/schema/types/type/REPORTED.gql b/backend/src/schema/types/type/REPORTED.gql deleted file mode 100644 index 5042672a7..000000000 --- a/backend/src/schema/types/type/REPORTED.gql +++ /dev/null @@ -1,43 +0,0 @@ -type REPORTED { - createdAt: String - reasonCategory: ReasonCategory - reasonDescription: String - submitter: User - @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user") - # not yet supported - # resource: ReportResource - # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") - type: String - @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]") - user: User - post: Post - comment: Comment -} - -# this list equals the strings of an array in file "webapp/constants/modals.js" -enum ReasonCategory { - other - discrimination_etc - pornographic_content_links - glorific_trivia_of_cruel_inhuman_acts - doxing - intentional_intimidation_stalking_persecution - advert_products_services_commercial - criminal_behavior_violation_german_law -} - -# not yet supported -# union ReportResource = User | Post | Comment - -enum ReportOrdering { - createdAt_asc - createdAt_desc -} - -type Query { - reports(orderBy: ReportOrdering): [REPORTED] -} - -type Mutation { - report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED -} diff --git a/backend/src/schema/types/type/REVIEWED.gql b/backend/src/schema/types/type/REVIEWED.gql new file mode 100644 index 000000000..aea005abe --- /dev/null +++ b/backend/src/schema/types/type/REVIEWED.gql @@ -0,0 +1,15 @@ +type REVIEWED { + createdAt: String! + updatedAt: String! + disable: Boolean! + closed: Boolean! + report: Report + # @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report") + moderator: User + resource: ReviewedResource +} +union ReviewedResource = User | Post | Comment + +type Mutation { + review(resourceId: ID!, disable: Boolean, closed: Boolean): REVIEWED +} diff --git a/backend/src/schema/types/type/Report.gql b/backend/src/schema/types/type/Report.gql new file mode 100644 index 000000000..49e5bdae3 --- /dev/null +++ b/backend/src/schema/types/type/Report.gql @@ -0,0 +1,25 @@ +type Report { + id: ID! + createdAt: String! + updatedAt: String! + rule: ReportRule! + disable: Boolean! + closed: Boolean! + filed: [FILED] + reviewed: [REVIEWED] + resource: ReportedResource +} + +union ReportedResource = User | Post | Comment + +enum ReportRule { + latestReviewUpdatedAtRules +} + +type Mutation { + fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report +} + +type Query { + reports(orderBy: ReportOrdering): [Report] +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 53e739988..243f45322 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -33,7 +33,6 @@ type User { coverImg: String deleted: Boolean disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") role: UserGroup! publicKey: String invitedBy: User @relation(name: "INVITED", direction: "IN") @@ -44,8 +43,6 @@ type User { about: String socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") - # createdAt: DateTime - # updatedAt: DateTime createdAt: String updatedAt: String diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 5054155fc..441fe47d5 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -10,6 +10,7 @@ import createLocation from './locations.js' import createEmailAddress from './emailAddresses.js' import createDonations from './donations.js' import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js' +import createReport from './reports.js' const factories = { Badge: createBadge, @@ -23,12 +24,13 @@ const factories = { EmailAddress: createEmailAddress, UnverifiedEmailAddress: createUnverifiedEmailAddresss, Donations: createDonations, + Report: createReport, } export const cleanDatabase = async (options = {}) => { const { driver = getDriver() } = options - const session = driver.session() const cypher = 'MATCH (n) DETACH DELETE n' + const session = driver.session() try { return await session.run(cypher) } finally { diff --git a/backend/src/seed/factories/reports.js b/backend/src/seed/factories/reports.js new file mode 100644 index 000000000..e2d5ec4dc --- /dev/null +++ b/backend/src/seed/factories/reports.js @@ -0,0 +1,7 @@ +export default function create() { + return { + factory: async ({ args, neodeInstance }) => { + return neodeInstance.create('Report', args) + }, + } +} diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 4040d7449..eaa2f7984 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -545,7 +545,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) authenticatedUser = null - await Promise.all([ + const comments = await Promise.all([ factory.create('Comment', { author: jennyRostock, id: 'c1', @@ -562,7 +562,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p3', }), factory.create('Comment', { - author: bobDerBaumeister, + author: jennyRostock, id: 'c5', postId: 'p3', }), @@ -602,6 +602,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }), ]) + const trollingComment = comments[0] await Promise.all([ democracy.relateTo(p3, 'post'), @@ -665,68 +666,107 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] louie.relateTo(p10, 'shouted'), ]) - const disableMutation = gql` - mutation($id: ID!) { - disable(id: $id) - } - ` - authenticatedUser = await bobDerBaumeister.toJson() - await Promise.all([ - mutate({ - mutation: disableMutation, - variables: { - id: 'p11', - }, - }), - mutate({ - mutation: disableMutation, - variables: { - id: 'c5', - }, - }), + const reports = await Promise.all([ + factory.create('Report'), + factory.create('Report'), + factory.create('Report'), ]) - authenticatedUser = null + const reportAgainstDagobert = reports[0] + const reportAgainstTrollingPost = reports[1] + const reportAgainstTrollingComment = reports[2] - // There is no error logged or the 'try' fails if this mutation is wrong. Why? - const reportMutation = gql` - mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report( - resourceId: $resourceId - reasonCategory: $reasonCategory - reasonDescription: $reasonDescription - ) { - type - } - } - ` - authenticatedUser = await huey.toJson() + // report resource first time await Promise.all([ - mutate({ - mutation: reportMutation, - variables: { - resourceId: 'c1', - reasonCategory: 'other', - reasonDescription: 'This comment is bigoted', - }, + reportAgainstDagobert.relateTo(jennyRostock, 'filed', { + resourceId: 'u7', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', }), - mutate({ - mutation: reportMutation, - variables: { - resourceId: 'p1', - reasonCategory: 'discrimination_etc', - reasonDescription: 'This post is bigoted', - }, + reportAgainstDagobert.relateTo(dagobert, 'belongsTo'), + reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', { + resourceId: 'p2', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", }), - mutate({ - mutation: reportMutation, - variables: { - resourceId: 'u1', - reasonCategory: 'doxing', - reasonDescription: 'This user is harassing me with bigoted remarks', - }, + reportAgainstTrollingPost.relateTo(p2, 'belongsTo'), + reportAgainstTrollingComment.relateTo(huey, 'filed', { + resourceId: 'c1', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + + // report resource a second time + await Promise.all([ + reportAgainstDagobert.relateTo(louie, 'filed', { + resourceId: 'u7', + reasonCategory: 'discrimination_etc', + reasonDescription: 'this user is attacking me for who I am!', + }), + reportAgainstDagobert.relateTo(dagobert, 'belongsTo'), + reportAgainstTrollingPost.relateTo(peterLustig, 'filed', { + resourceId: 'p2', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + }), + reportAgainstTrollingPost.relateTo(p2, 'belongsTo'), + + reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'filed', { + resourceId: 'c1', + reasonCategory: 'pornographic_content_links', + reasonDescription: 'This comment is porno!!!', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + + const disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + + // review resource first time + await Promise.all([ + reportAgainstDagobert.relateTo(bobDerBaumeister, 'reviewed', { + ...disableVariables, + resourceId: 'u7', + }), + dagobert.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingPost.relateTo(peterLustig, 'reviewed', { + ...disableVariables, + resourceId: 'p2', + }), + p2.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + + // second review of resource and close report + await Promise.all([ + reportAgainstDagobert.relateTo(peterLustig, 'reviewed', { + resourceId: 'u7', + disable: false, + closed: true, + }), + dagobert.update({ disabled: false, updatedAt: new Date().toISOString(), closed: true }), + reportAgainstTrollingPost.relateTo(bobDerBaumeister, 'reviewed', { + resourceId: 'p2', + disable: true, + closed: true, + }), + p2.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), + reportAgainstTrollingComment.relateTo(peterLustig, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + disable: true, + closed: true, + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), ]) - authenticatedUser = null await Promise.all( [...Array(30).keys()].map(i => { diff --git a/backend/src/server.js b/backend/src/server.js index 70eae86f1..053a3e4b3 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -6,6 +6,7 @@ import middleware from './middleware' import { neode as getNeode, getDriver } from './bootstrap/neo4j' import decode from './jwt/decode' import schema from './schema' +import webfinger from './activitypub/routes/webfinger' // check required configs and throw error // TODO check this directly in config file - currently not possible due to testsetup @@ -41,7 +42,10 @@ const createServer = options => { const server = new ApolloServer(Object.assign({}, defaults, options)) const app = express() + + app.set('driver', driver) app.use(helmet()) + app.use('/.well-known/', webfinger()) app.use(express.static('public')) server.applyMiddleware({ app, path: '/' }) diff --git a/backend/test/features/webfinger.feature b/backend/test/features/webfinger.feature index 72062839a..cbca5ac10 100644 --- a/backend/test/features/webfinger.feature +++ b/backend/test/features/webfinger.feature @@ -9,32 +9,6 @@ Feature: Webfinger discovery | Slug | | peter-lustiger | - Scenario: Search - When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost" - Then I receive the following json: - """ - { - "subject": "acct:peter-lustiger@localhost:4123", - "links": [ - { - "rel": "self", - "type": "application/activity+json", - "href": "http://localhost:4123/activitypub/users/peter-lustiger" - } - ] - } - """ - And I expect the Content-Type to be "application/jrd+json; charset=utf-8" - - Scenario: User does not exist - When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost" - Then I receive the following json: - """ - { - "error": "No record found for nonexisting@localhost." - } - """ - Scenario: Receiving an actor object When I send a GET request to "/activitypub/users/peter-lustiger" Then I receive the following json: diff --git a/backend/yarn.lock b/backend/yarn.lock index 7f0c95a43..8e2ca5446 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -33,12 +33,12 @@ resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== -"@babel/cli@~7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.7.0.tgz#8d10c9acb2acb362d7614a9493e1791c69100d89" - integrity sha512-jECEqAq6Ngf3pOhLSg7od9WKyrIacyh1oNNYtRXNn+ummSHCTXBamGywOAtiae34Vk7zKuQNnLvo2BKTMCoV4A== +"@babel/cli@~7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.7.4.tgz#38804334c8db40209f88c69a5c90998e60cca18b" + integrity sha512-O7mmzaWdm+VabWQmxuM8hqNrWGGihN83KfhPUzp2lAW4kzIMwBxujXkZbD4fMwKMYY9FXTbDvXsJqU+5XHXi4A== dependencies: - commander "^2.8.1" + commander "^4.0.1" convert-source-map "^1.1.0" fs-readdir-recursive "^1.1.0" glob "^7.0.0" @@ -56,18 +56,18 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.7.2": - version "7.7.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.2.tgz#ea5b99693bcfc058116f42fa1dd54da412b29d91" - integrity sha512-eeD7VEZKfhK1KUXGiyPFettgF3m513f8FoBSWiQ1xTvl1RAopLs42Wp9+Ze911I6H0N9lNqJMDgoZT7gHsipeQ== +"@babel/core@^7.1.0", "@babel/core@~7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.4.tgz#37e864532200cb6b50ee9a4045f5f817840166ab" + integrity sha512-+bYbx56j4nYBmpsWtnPUsKW3NdnYxbqyfrP2w9wILBuHzdfIKz9prieZK0DFPyIzkjYVUe4QkusGL07r5pXznQ== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.7.2" - "@babel/helpers" "^7.7.0" - "@babel/parser" "^7.7.2" - "@babel/template" "^7.7.0" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.7.2" + "@babel/generator" "^7.7.4" + "@babel/helpers" "^7.7.4" + "@babel/parser" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" convert-source-map "^1.7.0" debug "^4.1.0" json5 "^2.1.0" @@ -76,17 +76,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.7.2": - version "7.7.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.2.tgz#2f4852d04131a5e17ea4f6645488b5da66ebf3af" - integrity sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ== - dependencies: - "@babel/types" "^7.7.2" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - -"@babel/generator@^7.7.4": +"@babel/generator@^7.4.0", "@babel/generator@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" integrity sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg== @@ -145,15 +135,6 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/helper-function-name@^7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz#44a5ad151cfff8ed2599c91682dda2ec2c8430a3" - integrity sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q== - dependencies: - "@babel/helper-get-function-arity" "^7.7.0" - "@babel/template" "^7.7.0" - "@babel/types" "^7.7.0" - "@babel/helper-function-name@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" @@ -163,13 +144,6 @@ "@babel/template" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/helper-get-function-arity@^7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz#c604886bc97287a1d1398092bc666bc3d7d7aa2d" - integrity sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw== - dependencies: - "@babel/types" "^7.7.0" - "@babel/helper-get-function-arity@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" @@ -258,13 +232,6 @@ "@babel/template" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/helper-split-export-declaration@^7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz#1365e74ea6c614deeb56ebffabd71006a0eb2300" - integrity sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA== - dependencies: - "@babel/types" "^7.7.0" - "@babel/helper-split-export-declaration@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz#57292af60443c4a3622cf74040ddc28e68336fd8" @@ -282,14 +249,14 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/helpers@^7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.0.tgz#359bb5ac3b4726f7c1fde0ec75f64b3f4275d60b" - integrity sha512-VnNwL4YOhbejHb7x/b5F39Zdg5vIQpUUNzJwx0ww1EcVRt41bbGRZWhAURrfY32T5zTT3qwNOQFWpn+P0i0a2g== +"@babel/helpers@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.4.tgz#62c215b9e6c712dadc15a9a0dcab76c92a940302" + integrity sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg== dependencies: - "@babel/template" "^7.7.0" - "@babel/traverse" "^7.7.0" - "@babel/types" "^7.7.0" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" "@babel/highlight@^7.0.0": version "7.5.0" @@ -313,12 +280,7 @@ regenerator-runtime "^0.13.3" v8flags "^3.1.1" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2": - version "7.7.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.2.tgz#ea8334dc77416bfd9473eb470fd00d8245b3943b" - integrity sha512-DDaR5e0g4ZTb9aP7cpSZLkACEBdoLGwJDWgHtBhrGX7Q1RjhdoMOfexICj5cqTAtpowjGQWfcvfnQG7G2kAB5w== - -"@babel/parser@^7.7.4": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.4.tgz#75ab2d7110c2cf2fa949959afb05fa346d2231bb" integrity sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g== @@ -364,13 +326,13 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.7.4" -"@babel/plugin-proposal-throw-expressions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739" - integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw== +"@babel/plugin-proposal-throw-expressions@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.7.4.tgz#0321bd4acb699abef3006f7cd3d1b2c00daf1b82" + integrity sha512-yMcK1dM9Rv+Y5n62rKaHfRoRD4eOWIqYn4uy/Xu7C47rJKaR5JpQR905Hc/OL8EEaGNcEyuvjOtYdNAVXZKDZQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-throw-expressions" "^7.2.0" + "@babel/plugin-syntax-throw-expressions" "^7.7.4" "@babel/plugin-proposal-unicode-property-regex@^7.7.4": version "7.7.4" @@ -415,10 +377,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-throw-expressions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz#79001ee2afe1b174b1733cdc2fc69c9a46a0f1f8" - integrity sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg== +"@babel/plugin-syntax-throw-expressions@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.7.4.tgz#2e1e91485f9a35f1f71547717ccd8407a900092c" + integrity sha512-qtLTzzOJ1Co6tQ8pMqnsfRgeUfzExP90Tc6GISTC34O0lR6IDvymWLfVJFcJaQinz6reciQ4auALV3JM+b3D/Q== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -756,16 +718,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.4.0", "@babel/template@^7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.0.tgz#4fadc1b8e734d97f56de39c77de76f2562e597d0" - integrity sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.7.0" - "@babel/types" "^7.7.0" - -"@babel/template@^7.7.4": +"@babel/template@^7.4.0", "@babel/template@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" integrity sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw== @@ -774,22 +727,7 @@ "@babel/parser" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": - version "7.7.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.2.tgz#ef0a65e07a2f3c550967366b3d9b62a2dcbeae09" - integrity sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw== - dependencies: - "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.7.2" - "@babel/helper-function-name" "^7.7.0" - "@babel/helper-split-export-declaration" "^7.7.0" - "@babel/parser" "^7.7.2" - "@babel/types" "^7.7.2" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.7.4": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" integrity sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw== @@ -804,7 +742,7 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.7.0", "@babel/types@^7.7.2", "@babel/types@^7.7.4": +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA== @@ -879,10 +817,10 @@ "@hapi/hoek" "8.x.x" "@hapi/topo" "3.x.x" -"@hapi/joi@^16.1.7": - version "16.1.7" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.7.tgz#360857223a87bb1f5f67691537964c1b4908ed93" - integrity sha512-anaIgnZhNooG3LJLrTFzgGALTiO97zRA1UkvQHm9KxxoSiIzCozB3RCNCpDnfhTJD72QlrHA8nwGmNgpFFCIeg== +"@hapi/joi@^16.1.8": + version "16.1.8" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839" + integrity sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg== dependencies: "@hapi/address" "^2.1.2" "@hapi/formula" "^1.2.0" @@ -1453,10 +1391,10 @@ dependencies: "@types/yargs-parser" "*" -"@types/yup@0.26.24": - version "0.26.24" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.24.tgz#c24170b3a5c867b4fabd49fcc42fe45f780cb153" - integrity sha512-x0bhHnYjH5mZit4HivUYbTMO4LouOTGwp/LLxSL1mbJYVwNJtHYESH0ed2bwM1lkI2yDmsoCDYJnWEgHeJDACg== +"@types/yup@0.26.26": + version "0.26.26" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.26.tgz#2e7065384ac2b7711271d8790f26ac7d73f6a33d" + integrity sha512-5cLJLd8NIT7OfJLi7ScquRn/NWfmewBqJ92nT/FYfdpgKzyUNcR4n2BKEOQ7mOG8WuJXgomIvNl5P3sn9Akd4A== "@types/zen-observable@^0.8.0": version "0.8.0" @@ -1693,13 +1631,6 @@ apollo-datasource@^0.6.3: apollo-server-caching "^0.5.0" apollo-server-env "^2.4.3" -apollo-engine-reporting-protobuf@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.3.tgz#c8cf52aa799a2d8fc92bd59c5942bb2989d225aa" - integrity sha512-ikRiu2PJOOYoTfJ1gAPB58tmVwu8IF71J9u1jWJJ7tLO+RvGOvntA4kwImiKbHrc/zsAx12lPnpjjfn7tOF1AQ== - dependencies: - protobufjs "^6.8.6" - apollo-engine-reporting-protobuf@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.4.tgz#73a064f8c9f2d6605192d1673729c66ec47d9cb7" @@ -1788,7 +1719,7 @@ apollo-server-caching@^0.5.0: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.9.11, apollo-server-core@^2.9.12: +apollo-server-core@^2.9.12: version "2.9.12" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.12.tgz#c8ed48540762913242eef5fce0da8b59b131a1e8" integrity sha512-jhGr2R655PSwUUBweXDl+0F3oa74Elu5xXF+88ymUUej34EwBUCqz97wPqR07BEuyxaAlRfZwPMvKaHhMUKg5g== @@ -1828,10 +1759,10 @@ apollo-server-errors@^2.3.4: resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34" integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA== -apollo-server-express@^2.9.11, apollo-server-express@^2.9.7: - version "2.9.11" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.11.tgz#ae1b6161a563bc6aa0dd7456deec998236a4234a" - integrity sha512-38ZaPnahJPnibVii6i983Uu32iGR2xO5kUZvGxqH4lSPiy8V9Ph93TxeTmIlzHUqd8z4CguZsCV+ngd5KT7lFQ== +apollo-server-express@^2.9.12, apollo-server-express@^2.9.7: + version "2.9.12" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.12.tgz#e779ea2c107fcc63b0c9b888a4cbf0f65af6d505" + integrity sha512-4Ev8MY7m23mSzwO/BvLTy97a/68IP/wZoCRBn2R81OoZt9/GxlvvYZGvozJCXYsQt1qAbIT4Sn05LmqawsI98w== dependencies: "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" @@ -1839,8 +1770,8 @@ apollo-server-express@^2.9.11, apollo-server-express@^2.9.7: "@types/cors" "^2.8.4" "@types/express" "4.17.1" accepts "^1.3.5" - apollo-server-core "^2.9.11" - apollo-server-types "^0.2.7" + apollo-server-core "^2.9.12" + apollo-server-types "^0.2.8" body-parser "^1.18.3" cors "^2.8.4" express "^4.17.1" @@ -1864,15 +1795,6 @@ apollo-server-testing@~2.9.12: dependencies: apollo-server-core "^2.9.12" -apollo-server-types@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.7.tgz#832760017a9d55beda23bc7ba8af357802ee9b49" - integrity sha512-umbXyo7DVG6/lZIAL1D666e16+gZM/Y/kfPx2nn9KTSamIJHUDLs2p0es78RuUx2VS8XoKZDgL0BTkIi5Nt8Iw== - dependencies: - apollo-engine-reporting-protobuf "^0.4.3" - apollo-server-caching "^0.5.0" - apollo-server-env "^2.4.3" - apollo-server-types@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.8.tgz#729208a8dd72831af3aa4f1eb584022ada146e6b" @@ -1882,13 +1804,13 @@ apollo-server-types@^0.2.8: apollo-server-caching "^0.5.0" apollo-server-env "^2.4.3" -apollo-server@~2.9.11: - version "2.9.11" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.11.tgz#fafa7bc8f758689b0675c3300599a9b1128c58cd" - integrity sha512-BhMpBqgdEYN2SqjigRyEDUDKN3GwrYXzMZXUv/WD55HaKST8RAP4GiJp8lHswdeoUfr/9fsfvtVlrN++zJziuQ== +apollo-server@~2.9.12: + version "2.9.12" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.12.tgz#3fe28c361ee373d52ae38ca190869508b0c532c0" + integrity sha512-Q+qaBTgTxb2vwqyh7NTHs9rOmadbuKw34SgeAOLsCnr3MLVjisa50fL3nQrGbhOGfRaroF8SSZYgya0tvnefig== dependencies: - apollo-server-core "^2.9.11" - apollo-server-express "^2.9.11" + apollo-server-core "^2.9.12" + apollo-server-express "^2.9.12" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" @@ -2648,6 +2570,11 @@ commander@^3.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +commander@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c" + integrity sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3407,10 +3334,10 @@ eslint-plugin-import@~2.18.2: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~23.0.4: - version "23.0.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz#1ab81ffe3b16c5168efa72cbd4db14d335092aa0" - integrity sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw== +eslint-plugin-jest@~23.1.1: + version "23.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.1.1.tgz#1220ab53d5a4bf5c3c4cd07c0dabc6199d4064dd" + integrity sha512-2oPxHKNh4j1zmJ6GaCBuGcb8FVZU7YjFUOJzGOPnl9ic7VA/MGAskArLJiRIlnFUmi1EUxY+UiATAy8dv8s5JA== dependencies: "@typescript-eslint/experimental-utils" "^2.5.0" @@ -3463,10 +3390,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" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== -eslint@~6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.1.tgz#269ccccec3ef60ab32358a44d147ac209154b919" - integrity sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA== +eslint@~6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1" + integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -4173,12 +4100,12 @@ graphql-middleware@~4.0.2: dependencies: graphql-tools "^4.0.5" -graphql-shield@~7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.2.tgz#65d51528b4e36c4a5023276267abbd1abb57ce0b" - integrity sha512-ZgCQ9efzgehkIVMFoxaOubnvwHVqthtsIXA+xRu8B4ga2Ch/EHiswNCDKpTXTBQZmjeDCSHeAj4XDmbzR4Trxw== +graphql-shield@~7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.4.tgz#fdff8066f9fbb86b363e7dd6a9cf295dbbf8a09b" + integrity sha512-+SEz/tKx2uJAbMKzS7X0hCUWsZo54J8SARhXb5jNDG/RKur44mjIGfBnuBRszw73+dUdBvTlLl1j1WKwm0ZhEA== dependencies: - "@types/yup" "0.26.24" + "@types/yup" "0.26.26" object-hash "^2.0.0" yup "^0.27.0" @@ -6936,25 +6863,6 @@ property-expr@^1.5.0: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f" integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g== -protobufjs@^6.8.6: - version "6.8.8" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" - integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.0" - "@types/node" "^10.1.0" - long "^4.0.0" - proxy-addr@~2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 17481befe..9f62a2818 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -129,8 +129,8 @@ Given('somebody reported the following posts:', table => { .create('User', submitter) .authenticateAs(submitter) .mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { - type + fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { + id } }`, { resourceId, diff --git a/deployment/minikube/README.md b/deployment/minikube/README.md index e77ddd667..342675b1b 100644 --- a/deployment/minikube/README.md +++ b/deployment/minikube/README.md @@ -9,7 +9,7 @@ open your minikube dashboard: $ minikube dashboard ``` -This will give you an overview. Some of the steps below need some timing to make ressources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that. +This will give you an overview. Some of the steps below need some timing to make resources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that. Follow the installation instruction for [Human Connection](../human-connection/README.md). If all the pods and services have settled and everything looks green in your diff --git a/docker-compose.override.yml b/docker-compose.override.yml index df7d6ac92..56f13939e 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -15,7 +15,7 @@ services: - ./webapp:/nitro-web environment: - NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/` - - PUBLIC_REGISTRATION=true + - PUBLIC_REGISTRATION=false command: yarn run dev backend: build: @@ -29,7 +29,7 @@ services: - SMTP_PORT=25 - SMTP_IGNORE_TLS=true - "DEBUG=${DEBUG}" - - PUBLIC_REGISTRATION=true + - PUBLIC_REGISTRATION=false maintenance: image: humanconnection/maintenance:latest build: diff --git a/features/support/steps.js b/features/support/steps.js new file mode 100644 index 000000000..923dc9766 --- /dev/null +++ b/features/support/steps.js @@ -0,0 +1,48 @@ +// features/support/steps.js +import { Given, When, Then, After, AfterAll } from 'cucumber' +import Factory from '../../backend/src/seed/factories' +import dotenv from 'dotenv' +import expect from 'expect' + +const debug = require('debug')('ea:test:steps') +const factory = Factory() + + +After(async () => { + await factory.cleanDatabase() +}) + +Given('our CLIENT_URI is {string}', function (string) { + expect(string).toEqual('http://localhost:3000') + // This is just for documentation. When you see URLs in the response of + // scenarios you, should be able to tell that it's coming from this + // environment variable. +}); + +Given('we have the following users in our database:', function (dataTable) { + return Promise.all(dataTable.hashes().map(({ slug, name }) => { + return factory.create('User', { + name, + slug, + }) + })) +}) + +When('I send a GET request to {string}', async function (pathname) { + const response = await this.get(pathname) + this.lastContentType = response.lastContentType + + this.lastResponses.push(response.lastResponse) + this.statusCode = response.statusCode +}) + +Then('the server responds with a HTTP Status {int} and the following json:', function (statusCode, docString) { + expect(this.statusCode).toEqual(statusCode) + const [ lastResponse ] = this.lastResponses + expect(JSON.parse(lastResponse)).toMatchObject(JSON.parse(docString)) +}) + +Then('the Content-Type is {string}', function (contentType) { + expect(this.lastContentType).toEqual(contentType) +}) + diff --git a/features/webfinger.feature b/features/webfinger.feature new file mode 100644 index 000000000..1a17e7ea3 --- /dev/null +++ b/features/webfinger.feature @@ -0,0 +1,36 @@ +Feature: Webfinger discovery + From an external server, e.g. Mastodon + I want to search for an actor alias + In order to follow the actor + + Background: + Given our CLIENT_URI is "http://localhost:3000" + And we have the following users in our database: + | name | slug | + | Peter Lustiger | peter-lustiger | + + Scenario: Search a user + When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost" + Then the server responds with a HTTP Status 200 and the following json: + """ + { + "subject": "acct:peter-lustiger@localhost:3000", + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": "http://localhost:3000/activitypub/users/peter-lustiger" + } + ] + } + """ + And the Content-Type is "application/jrd+json; charset=utf-8" + + Scenario: Search without result + When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost" + Then the server responds with a HTTP Status 404 and the following json: + """ + { + "error": "No record found for \"nonexisting@localhost\"." + } + """ diff --git a/features/world.js b/features/world.js new file mode 100644 index 000000000..f46a63d4e --- /dev/null +++ b/features/world.js @@ -0,0 +1,38 @@ +import { setWorldConstructor } from 'cucumber' +import request from 'request' + +class CustomWorld { + constructor () { + // webFinger.feature + this.lastResponses = [] + this.lastContentType = null + this.lastInboxUrl = null + this.lastActivity = null + // object-article.feature + this.statusCode = null + } + get (pathname) { + return new Promise((resolve, reject) => { + request(`http://localhost:4000/${this.replaceSlashes(pathname)}`, { + headers: { + 'Accept': 'application/activity+json' + }}, (error, response, body) => { + if (!error) { + resolve({ + lastResponse: body, + lastContentType: response.headers['content-type'], + statusCode: response.statusCode + }) + } else { + reject(error) + } + }) + }) + } + + replaceSlashes (pathname) { + return pathname.replace(/^\/+/, '') + } +} + +setWorldConstructor(CustomWorld) diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index 5d4827586..a180a18a2 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,4 +1,4 @@ -FROM neo4j:3.5.12-enterprise +FROM neo4j:3.5.13-enterprise LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" ARG BUILD_COMMIT diff --git a/neo4j/change_disabled_relationship_to_report_node.sh b/neo4j/change_disabled_relationship_to_report_node.sh new file mode 100755 index 000000000..2f44b8e59 --- /dev/null +++ b/neo4j/change_disabled_relationship_to_report_node.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +ENV_FILE=$(dirname "$0")/.env +[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" + +if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then + echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." + echo "Database manipulation is not possible without connecting to the database." + echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" +fi + +until echo 'RETURN "Connection successful" as info;' | cypher-shell +do + echo "Connecting to neo4j failed, trying again..." + sleep 1 +done + +echo " +// convert old DISABLED to new REVIEWED-Report-BELONGS_TO structure +MATCH (moderator:User)-[disabled:DISABLED]->(disabledResource) +WHERE disabledResource:User OR disabledResource:Comment OR disabledResource:Post +DELETE disabled +CREATE (moderator)-[review:REVIEWED]->(report:Report)-[:BELONGS_TO]->(disabledResource) +SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true +SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false + +// if disabledResource has no filed report, then create a moderators default filed report +WITH moderator, disabledResource, report +OPTIONAL MATCH (disabledResourceReporter:User)-[existingFiledReport:FILED]->(disabledResource) +FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NULL THEN [1] ELSE [] END | + CREATE (moderator)-[addModeratorReport:FILED]->(report) + SET addModeratorReport.createdAt = toString(datetime()), addModeratorReport.reasonCategory = 'other', addModeratorReport.reasonDescription = 'Old DISABLED relations didn't enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.' +) +FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NOT NULL THEN [1] ELSE [] END | + CREATE (disabledResourceReporter)-[moveModeratorReport:FILED]->(report) + SET moveModeratorReport = existingFiledReport + DELETE existingFiledReport +) + +RETURN disabledResource {.id}; +" | cypher-shell + +echo " +// for FILED resources without DISABLED relation which are handled above, create new FILED-Report-BELONGS_TO structure +MATCH (reporter:User)-[oldReport:REPORTED]->(notDisabledResource) +WHERE notDisabledResource:User OR notDisabledResource:Comment OR notDisabledResource:Post +MERGE (report:Report)-[:BELONGS_TO]->(notDisabledResource) +ON CREATE SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false +CREATE (reporter)-[filed:FILED]->(report) +SET report = oldReport +DELETE oldReport + +RETURN notDisabledResource {.id}; +" | cypher-shell + diff --git a/neo4j/change_report_node_to_relationship.sh b/neo4j/change_report_node_to_relationship.sh deleted file mode 100755 index f8dd639be..000000000 --- a/neo4j/change_report_node_to_relationship.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -ENV_FILE=$(dirname "$0")/.env -[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" - -if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then - echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." - echo "Database manipulation is not possible without connecting to the database." - echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" -fi - -until echo 'RETURN "Connection successful" as info;' | cypher-shell -do - echo "Connecting to neo4j failed, trying again..." - sleep 1 -done - -echo " -MATCH (submitter:User)-[:REPORTED]->(report:Report)-[:REPORTED]->(resource) -DETACH DELETE report -CREATE (submitter)-[reported:REPORTED]->(resource) -SET reported.createdAt = toString(datetime()) -SET reported.reasonCategory = 'other' -SET reported.reasonDescription = '!!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.' -RETURN reported; -" | cypher-shell diff --git a/package.json b/package.json index d403bfe5b..ac7e575df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "nitro-cypress", + "name": "human-connection", "version": "0.1.11", - "description": "Fullstack tests with cypress for Human Connection", + "description": "Fullstack and API tests with cypress and cucumber for Human Connection", "author": "Human Connection gGmbh", "license": "MIT", "cypress-cucumber-preprocessor": { @@ -16,19 +16,26 @@ "cypress:setup": "run-p cypress:backend cypress:webapp", "cypress:run": "cross-env cypress run --browser chromium", "cypress:open": "cross-env cypress open --browser chromium", + "cucumber:setup": "cd backend && yarn run dev", + "cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit", "version": "auto-changelog -p" }, "devDependencies": { + "@babel/core": "^7.7.2", + "@babel/preset-env": "^7.7.4", + "@babel/register": "^7.7.4", "auto-changelog": "^1.16.2", "bcryptjs": "^2.4.3", "codecov": "^3.6.1", "cross-env": "^6.0.3", - "cypress": "^3.6.1", - "cypress-cucumber-preprocessor": "^1.16.2", + "cucumber": "^6.0.5", + "cypress": "^3.7.0", + "cypress-cucumber-preprocessor": "^1.17.0", "cypress-file-upload": "^3.5.0", - "cypress-plugin-retries": "^1.4.0", + "cypress-plugin-retries": "^1.5.0", "date-fns": "^2.8.1", "dotenv": "^8.2.0", + "expect": "^24.9.0", "faker": "Marak/faker.js#master", "graphql-request": "^1.8.2", "neo4j-driver": "^1.7.6", diff --git a/webapp/components/CommentList/CommentList.vue b/webapp/components/CommentList/CommentList.vue index 888c167e9..25ed62f68 100644 --- a/webapp/components/CommentList/CommentList.vue +++ b/webapp/components/CommentList/CommentList.vue @@ -1,19 +1,9 @@ - - diff --git a/webapp/components/ContentMenu/ContentMenu.spec.js b/webapp/components/ContentMenu/ContentMenu.spec.js index 485c43145..8f93aa4a4 100644 --- a/webapp/components/ContentMenu/ContentMenu.spec.js +++ b/webapp/components/ContentMenu/ContentMenu.spec.js @@ -85,7 +85,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'post.menu.delete') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('delete') + expect(openModalSpy).toHaveBeenCalledWith('confirm', 'delete') }) }) @@ -166,7 +166,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'comment.menu.delete') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('delete') + expect(openModalSpy).toHaveBeenCalledWith('confirm', 'delete') }) }) @@ -332,7 +332,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.contribution.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) it('can release comments', () => { @@ -350,7 +350,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.comment.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) it('can release users', () => { @@ -368,7 +368,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.user.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) it('can release organizations', () => { @@ -386,7 +386,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.organization.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) }) diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index f0d9dc8d3..d4c567437 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -70,7 +70,7 @@ export default { routes.push({ name: this.$t(`post.menu.delete`), callback: () => { - this.openModal('delete') + this.openModal('confirm', 'delete') }, icon: 'trash', }) @@ -108,7 +108,7 @@ export default { routes.push({ name: this.$t(`comment.menu.delete`), callback: () => { - this.openModal('delete') + this.openModal('confirm', 'delete') }, icon: 'trash', }) @@ -137,7 +137,7 @@ export default { routes.push({ name: this.$t(`release.${this.resourceType}.title`), callback: () => { - this.openModal('release', this.resource.id) + this.openModal('release') }, icon: 'eye', }) @@ -190,13 +190,13 @@ export default { } toggleMenu() }, - openModal(dialog) { + openModal(dialog, modalDataName = null) { this.$store.commit('modal/SET_OPEN', { name: dialog, data: { type: this.resourceType, resource: this.resource, - modalsData: this.modalsData, + modalData: modalDataName ? this.modalsData[modalDataName] : {}, }, }) }, diff --git a/webapp/components/DropdownFilter/DropdownFilter.spec.js b/webapp/components/DropdownFilter/DropdownFilter.spec.js index 8020a487f..8fb1b408f 100644 --- a/webapp/components/DropdownFilter/DropdownFilter.spec.js +++ b/webapp/components/DropdownFilter/DropdownFilter.spec.js @@ -64,9 +64,9 @@ describe('DropdownFilter.vue', () => { expect(unreadLink.text()).toEqual('Unread') }) - it('clicking on menu item emits filterNotifications', () => { + it('clicking on menu item emits filter', () => { allLink.trigger('click') - expect(wrapper.emitted().filterNotifications[0]).toEqual( + expect(wrapper.emitted().filter[0]).toEqual( propsData.filterOptions.filter(option => option.label === 'All'), ) }) diff --git a/webapp/components/DropdownFilter/DropdownFilter.story.js b/webapp/components/DropdownFilter/DropdownFilter.story.js index 0703c5c47..9bd750ac1 100644 --- a/webapp/components/DropdownFilter/DropdownFilter.story.js +++ b/webapp/components/DropdownFilter/DropdownFilter.story.js @@ -20,10 +20,10 @@ storiesOf('DropdownFilter', module) selected: filterOptions[0].label, }), methods: { - filterNotifications: action('filterNotifications'), + filter: action('filter'), }, template: ``, diff --git a/webapp/components/DropdownFilter/DropdownFilter.vue b/webapp/components/DropdownFilter/DropdownFilter.vue index 2a3637f41..bfa78e709 100644 --- a/webapp/components/DropdownFilter/DropdownFilter.vue +++ b/webapp/components/DropdownFilter/DropdownFilter.vue @@ -25,7 +25,7 @@ class="dropdown-menu-item" :route="item.route" :parents="item.parents" - @click.stop.prevent="filterNotifications(item.route, toggleMenu)" + @click.stop.prevent="filter(item.route, toggleMenu)" > {{ item.route.label }} @@ -44,8 +44,8 @@ export default { filterOptions: { type: Array, default: () => [] }, }, methods: { - filterNotifications(option, toggleMenu) { - this.$emit('filterNotifications', option) + filter(option, toggleMenu) { + this.$emit('filter', option) toggleMenu() }, }, diff --git a/webapp/components/Modal.vue b/webapp/components/Modal.vue index 3c83a0922..84a1871b5 100644 --- a/webapp/components/Modal.vue +++ b/webapp/components/Modal.vue @@ -23,11 +23,11 @@ @close="close" /> diff --git a/webapp/components/Modal/ConfirmModal.vue b/webapp/components/Modal/ConfirmModal.vue index 147258849..8297e6d0f 100644 --- a/webapp/components/Modal/ConfirmModal.vue +++ b/webapp/components/Modal/ConfirmModal.vue @@ -77,7 +77,7 @@ export default { }, 500) }, 1500) } catch (err) { - this.success = false + this.isOpen = false } finally { this.loading = false } diff --git a/webapp/components/Modal/DisableModal.spec.js b/webapp/components/Modal/DisableModal.spec.js index 8bf796921..a1bc2046e 100644 --- a/webapp/components/Modal/DisableModal.spec.js +++ b/webapp/components/Modal/DisableModal.spec.js @@ -26,9 +26,7 @@ describe('DisableModal.vue', () => { $apollo: { mutate: jest .fn() - .mockResolvedValueOnce({ - enable: 'u4711', - }) + .mockResolvedValueOnce() .mockRejectedValue({ message: 'Not Authorised!', }), @@ -159,11 +157,13 @@ describe('DisableModal.vue', () => { expect(mocks.$apollo.mutate).toHaveBeenCalled() }) - it('passes id to mutation', () => { + it('passes parameters to mutation', () => { const calls = mocks.$apollo.mutate.mock.calls const [[{ variables }]] = calls - expect(variables).toEqual({ - id: 'u4711', + expect(variables).toMatchObject({ + resourceId: 'u4711', + disable: true, + closed: false, }) }) diff --git a/webapp/components/Modal/DisableModal.vue b/webapp/components/Modal/DisableModal.vue index 1e778ec68..d80ec0f55 100644 --- a/webapp/components/Modal/DisableModal.vue +++ b/webapp/components/Modal/DisableModal.vue @@ -54,11 +54,13 @@ export default { // await this.modalData.buttons.confirm.callback() await this.$apollo.mutate({ mutation: gql` - mutation($id: ID!) { - disable(id: $id) + mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) { + review(resourceId: $resourceId, disable: $disable, closed: $closed) { + disable + } } `, - variables: { id: this.id }, + variables: { resourceId: this.id, disable: true, closed: false }, }) this.$toast.success(this.$t('disable.success')) this.isOpen = false @@ -67,6 +69,7 @@ export default { }, 1000) } catch (err) { this.$toast.error(err.message) + this.isOpen = false } }, }, diff --git a/webapp/components/Modal/ReportModal.vue b/webapp/components/Modal/ReportModal.vue index 9b155e8b6..00fed2646 100644 --- a/webapp/components/Modal/ReportModal.vue +++ b/webapp/components/Modal/ReportModal.vue @@ -149,6 +149,7 @@ export default { default: this.$toast.error(err.message) } + this.isOpen = false this.loading = false }) }, diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue index 1cf575574..30b86e8c2 100644 --- a/webapp/components/PasswordReset/Request.vue +++ b/webapp/components/PasswordReset/Request.vue @@ -46,7 +46,7 @@ + diff --git a/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js b/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js new file mode 100644 index 000000000..93f48ff9e --- /dev/null +++ b/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js @@ -0,0 +1,93 @@ +import { config, mount, RouterLinkStub } from '@vue/test-utils' +import Vuex from 'vuex' +import FiledReportsTable from './FiledReportsTable' +import { reports } from '~/components/features/ReportList/ReportList.story.js' + +const localVue = global.localVue + +localVue.filter('truncate', string => string) + +config.stubs['client-only'] = '' + +describe('FiledReportsTable.vue', () => { + let wrapper, mocks, propsData, stubs, filed + + beforeEach(() => { + mocks = { + $t: jest.fn(string => string), + } + stubs = { + NuxtLink: RouterLinkStub, + } + propsData = {} + }) + + describe('mount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + getters: { + 'auth/isModerator': () => true, + 'auth/user': () => { + return { id: 'moderator' } + }, + }, + }) + return mount(FiledReportsTable, { + propsData, + mocks, + localVue, + store, + stubs, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + describe('given reports', () => { + beforeEach(() => { + filed = reports.map(report => report.filed) + propsData.filed = filed[0] + wrapper = Wrapper() + }) + + it('renders a table', () => { + expect(wrapper.find('.ds-table').exists()).toBe(true) + }) + + it('has 4 columns', () => { + expect(wrapper.findAll('.ds-table-col')).toHaveLength(4) + }) + + describe('FiledReport', () => { + it('renders the reporting user', () => { + const userSlug = wrapper.find('[data-test="filing-user"]') + expect(userSlug.text()).toContain('@community-moderator') + }) + + it('renders the reported date', () => { + const date = wrapper.find('[data-test="filed-date"]') + expect(date.text()).toEqual('10/02/2019') + }) + + it('renders the category text', () => { + const columns = wrapper.findAll('.ds-table-col') + const reasonCategory = columns.filter( + category => + category.text() === 'report.reason.category.options.pornographic_content_links', + ) + expect(reasonCategory.exists()).toBe(true) + }) + + it("renders the Post's content", () => { + const columns = wrapper.findAll('.ds-table-col') + const reasonDescription = columns.filter( + column => column.text() === 'This comment is porno!!!', + ) + expect(reasonDescription.exists()).toBe(true) + }) + }) + }) + }) +}) diff --git a/webapp/components/features/FiledReportsTable/FiledReportsTable.story.js b/webapp/components/features/FiledReportsTable/FiledReportsTable.story.js new file mode 100644 index 000000000..ec17b06e3 --- /dev/null +++ b/webapp/components/features/FiledReportsTable/FiledReportsTable.story.js @@ -0,0 +1,25 @@ +import { storiesOf } from '@storybook/vue' +import { withA11y } from '@storybook/addon-a11y' +import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable' +import helpers from '~/storybook/helpers' +import { reports } from '~/components/features/ReportList/ReportList.story.js' + +storiesOf('FiledReportsTable', module) + .addDecorator(withA11y) + .addDecorator(helpers.layout) + .add('with filed reports', () => ({ + components: { FiledReportsTable }, + store: helpers.store, + data: () => ({ + filed: reports[0].filed, + }), + template: ` + + + + + +
+ +
`, + })) diff --git a/webapp/components/features/FiledReportsTable/FiledReportsTable.vue b/webapp/components/features/FiledReportsTable/FiledReportsTable.vue new file mode 100644 index 000000000..d54d1116e --- /dev/null +++ b/webapp/components/features/FiledReportsTable/FiledReportsTable.vue @@ -0,0 +1,73 @@ + + + + diff --git a/webapp/components/features/ReportList/ReportList.spec.js b/webapp/components/features/ReportList/ReportList.spec.js new file mode 100644 index 000000000..cfd83b4a6 --- /dev/null +++ b/webapp/components/features/ReportList/ReportList.spec.js @@ -0,0 +1,77 @@ +import { config, mount } from '@vue/test-utils' +import Vuex from 'vuex' +import ReportList from './ReportList' +import { reports } from './ReportList.story' +import ReportsTable from '~/components/features/ReportsTable/ReportsTable' +import DropdownFilter from '~/components/DropdownFilter/DropdownFilter' + +const localVue = global.localVue + +config.stubs['client-only'] = '' +config.stubs['nuxt-link'] = '' + +describe('ReportList', () => { + let mocks, mutations, getters, wrapper + + beforeEach(() => { + mocks = { + $apollo: { + mutate: jest + .fn() + .mockResolvedValueOnce({ + data: { review: { disable: true, resourceId: 'some-resource', closed: true } }, + }) + .mockRejectedValue({ message: 'Unable to review' }), + }, + $t: jest.fn(), + $toast: { + error: jest.fn(message => message), + }, + } + mutations = { + 'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(), + } + getters = { + 'auth/user': () => { + return { slug: 'awesome-user' } + }, + 'auth/isModerator': () => true, + } + }) + + describe('mount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + mutations, + getters, + }) + return mount(ReportList, { mocks, localVue, store }) + } + + describe('renders children components', () => { + beforeEach(async () => { + wrapper = Wrapper() + }) + + it('renders DropdownFilter', () => { + expect(wrapper.find(DropdownFilter).exists()).toBe(true) + }) + + it('renders ReportsTable', () => { + expect(wrapper.find(ReportsTable).exists()).toBe(true) + }) + }) + + describe('confirm is emitted by reports table', () => { + beforeEach(async () => { + wrapper = Wrapper() + wrapper.setData({ reports }) + wrapper.find(ReportsTable).vm.$emit('confirm', reports[0]) + }) + + it('calls modal/SET_OPEN', () => { + expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1) + }) + }) + }) +}) diff --git a/webapp/components/features/ReportList/ReportList.story.js b/webapp/components/features/ReportList/ReportList.story.js new file mode 100644 index 000000000..e126f1e64 --- /dev/null +++ b/webapp/components/features/ReportList/ReportList.story.js @@ -0,0 +1,193 @@ +import { storiesOf } from '@storybook/vue' +import { withA11y } from '@storybook/addon-a11y' +import { action } from '@storybook/addon-actions' +import { post } from '~/components/PostCard/PostCard.story.js' +import { user } from '~/components/User/User.story.js' +import helpers from '~/storybook/helpers' +import ReportList from './ReportList' +import DropdownFilter from '~/components/DropdownFilter/DropdownFilter' +import ReportsTable from '~/components/features/ReportsTable/ReportsTable' +helpers.init() + +export const reports = [ + { + __typename: 'Report', + closed: false, + createdAt: '2019-10-29T15:36:02.106Z', + updatedAt: '2019-12-02T15:56:35.651Z', + disable: false, + filed: [ + { + __typename: 'FILED', + createdAt: '2019-10-02T15:56:35.676Z', + reasonCategory: 'pornographic_content_links', + reasonDescription: 'This comment is porno!!!', + submitter: { + ...user, + name: 'Community moderator', + id: 'community-moderator', + slug: 'community-moderator', + }, + }, + ], + resource: { + __typename: 'Comment', + id: 'b6b38937-3efc-4d5e-b12c-549e4d6551a5', + createdAt: '2019-10-29T15:38:25.184Z', + updatedAt: '2019-10-30T15:38:25.184Z', + disabled: false, + deleted: false, + content: + '

@peter-lustig

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.

', + contentExcerpt: + '

@peter-lustig

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra …

', + post, + author: user, + }, + reviewed: [ + { + updatedAt: '2019-10-30T15:38:25.184Z', + moderator: { + __typename: 'User', + ...user, + name: 'Moderator', + id: 'moderator', + slug: 'moderator', + }, + }, + { + updatedAt: '2019-10-29T15:38:25.184Z', + moderator: { + __typename: 'User', + ...user, + name: 'Peter Lustig', + id: 'u3', + slug: 'peter-lustig', + }, + }, + ], + }, + { + __typename: 'Report', + closed: false, + createdAt: '2019-10-31T15:36:02.106Z', + updatedAt: '2019-12-03T15:56:35.651Z', + disable: true, + filed: [ + { + __typename: 'FILED', + createdAt: '2019-10-31T15:36:02.106Z', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + submitter: { + ...user, + name: 'Modertation team', + id: 'moderation-team', + slug: 'moderation-team', + }, + }, + ], + resource: { + __typename: 'Post', + author: { + ...user, + id: 'u7', + name: 'Dagobert', + slug: 'dagobert', + }, + deleted: false, + disabled: false, + id: 'p2', + slug: 'bigoted-post', + title: "I'm a bigoted post!", + }, + reviewed: null, + }, + { + __typename: 'Report', + closed: true, + createdAt: '2019-10-30T15:36:02.106Z', + updatedAt: '2019-12-01T15:56:35.651Z', + disable: true, + filed: [ + { + __typename: 'FILED', + createdAt: '2019-10-30T15:36:02.106Z', + reasonCategory: 'discrimination_etc', + reasonDescription: 'this user is attacking me for who I am!', + submitter: { + ...user, + name: 'Helpful user', + id: 'helpful-user', + slug: 'helpful-user', + }, + }, + ], + resource: { + __typename: 'User', + commentedCount: 0, + contributionsCount: 0, + deleted: false, + disabled: true, + followedByCount: 0, + id: 'u5', + name: 'Abusive user', + slug: 'abusive-user', + }, + reviewed: [ + { + updatedAt: '2019-12-01T15:56:35.651Z', + moderator: { + __typename: 'User', + ...user, + name: 'Peter Lustig', + id: 'u3', + slug: 'peter-lustig', + }, + }, + { + updatedAt: '2019-11-30T15:56:35.651Z', + moderator: { + __typename: 'User', + ...user, + name: 'Moderator', + id: 'moderator', + slug: 'moderator', + }, + }, + ], + }, +] +const unreviewedReports = reports.filter(report => !report.reviewed) +const reviewedReports = reports.filter(report => report.reviewed) +const closedReports = reports.filter(report => report.closed) +const filterOptions = [ + { label: 'All', value: reports }, + { label: 'Unreviewed', value: unreviewedReports }, + { label: 'Reviewed', value: reviewedReports }, + { label: 'Closed', value: closedReports }, +] + +storiesOf('ReportList', module) + .addDecorator(withA11y) + .addDecorator(helpers.layout) + .add('with reports', () => ({ + components: { ReportList, DropdownFilter, ReportsTable }, + store: helpers.store, + data: () => ({ + filterOptions, + selected: filterOptions[0].label, + reports, + }), + methods: { + openModal: action('openModal'), + filter: action('filter'), + }, + template: ` +
+

Reports

+ +
+ +
`, + })) diff --git a/webapp/components/features/ReportList/ReportList.vue b/webapp/components/features/ReportList/ReportList.vue new file mode 100644 index 000000000..85b74d7cc --- /dev/null +++ b/webapp/components/features/ReportList/ReportList.vue @@ -0,0 +1,142 @@ + + + + diff --git a/webapp/components/features/ReportRow/ReportRow.spec.js b/webapp/components/features/ReportRow/ReportRow.spec.js new file mode 100644 index 000000000..352011a28 --- /dev/null +++ b/webapp/components/features/ReportRow/ReportRow.spec.js @@ -0,0 +1,187 @@ +import { config, mount, RouterLinkStub } from '@vue/test-utils' +import Vuex from 'vuex' +import ReportRow from './ReportRow.vue' +import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon' +import { reports } from '~/components/features/ReportList/ReportList.story.js' + +const localVue = global.localVue + +config.stubs['client-only'] = '' + +describe('ReportRow', () => { + let propsData, mocks, stubs, getters, wrapper + + beforeEach(() => { + propsData = {} + mocks = { + $t: jest.fn(string => string), + } + stubs = { + NuxtLink: RouterLinkStub, + } + getters = { + 'auth/user': () => { + return { slug: 'awesome-user' } + }, + 'auth/isModerator': () => true, + } + }) + + describe('given a report ', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + getters, + }) + return mount(ReportRow, { propsData, mocks, stubs, localVue, store }) + } + + describe('has not been closed', () => { + let confirmButton + beforeEach(() => { + propsData = { ...propsData, report: reports[1] } + wrapper = Wrapper() + confirmButton = wrapper.find('.ds-button-danger') + }) + + it('renders a confirm button', () => { + expect(confirmButton.exists()).toBe(true) + }) + + it('emits confirm event', () => { + confirmButton.trigger('click') + expect(wrapper.emitted('confirm-report')).toHaveLength(1) + }) + }) + + describe('has been closed', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[2] } + wrapper = Wrapper() + }) + + it('renders a decided text', () => { + const decidedTitle = wrapper + .findAll('.title') + .filter(title => title.text() === 'moderation.reports.decided') + expect(decidedTitle.exists()).toBe(true) + }) + }) + + describe('has not been reviewed', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[1] } + wrapper = Wrapper() + }) + + it('renders its current status', () => { + expect(wrapper.find('.status-line').text()).toEqual('moderation.reports.enabled') + }) + }) + + describe('has been reviewed', () => { + describe('and disabled', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[2] } + wrapper = Wrapper() + }) + it('renders the disabled icon', () => { + expect( + wrapper + .find('.status-line') + .find(BaseIcon) + .props().name, + ).toEqual('eye-slash') + }) + + it('renders its current status', () => { + expect(wrapper.find('.status-line').text()).toEqual('moderation.reports.disabledBy') + }) + }) + + describe('and enabled', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[0] } + wrapper = Wrapper() + }) + it('renders the enabled icon', () => { + expect( + wrapper + .find('.status-line') + .find(BaseIcon) + .props().name, + ).toEqual('eye') + }) + + it('renders its current status', () => { + expect(wrapper.find('.status-line').text()).toEqual('moderation.reports.enabledBy') + }) + + it('renders the moderator who reviewed the resource', () => { + const username = wrapper.find('[data-test="report-reviewer"]') + expect(username.text()).toContain('@moderator') + }) + }) + }) + + describe('concerns a Comment', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[0] } + wrapper = Wrapper() + }) + + it('renders a comments icon', () => { + const commentsIcon = wrapper.find(BaseIcon).props().name + expect(commentsIcon).toEqual('comments') + }) + + it('renders a link to the post, with the comment contentExcerpt', () => { + const postLink = wrapper.find('.title') + expect(postLink.text()).toEqual('@peter-lustig Lorem ipsum dolor sit amet, …') + }) + + it('renders the author', () => { + const userSlug = wrapper.find('[data-test="report-author"]') + expect(userSlug.text()).toContain('@louie') + }) + }) + + describe('concerns a Post', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[1] } + wrapper = Wrapper() + }) + + it('renders a bookmark icon', () => { + const postIcon = wrapper.find(BaseIcon).props().name + expect(postIcon).toEqual('bookmark') + }) + + it('renders a link to the post', () => { + const postLink = wrapper.find('.title') + expect(postLink.text()).toEqual("I'm a bigoted post!") + }) + + it('renders the author', () => { + const username = wrapper.find('[data-test="report-author"]') + expect(username.text()).toContain('@dagobert') + }) + }) + + describe('concerns a User', () => { + beforeEach(() => { + propsData = { ...propsData, report: reports[2] } + wrapper = Wrapper() + }) + + it('renders a user icon', () => { + const userIcon = wrapper.find(BaseIcon).props().name + expect(userIcon).toEqual('user') + }) + + it('renders a link to the user profile', () => { + const userLink = wrapper.find('[data-test="report-content"]') + expect(userLink.text()).toContain('@abusive-user') + }) + }) + }) +}) diff --git a/webapp/components/features/ReportRow/ReportRow.vue b/webapp/components/features/ReportRow/ReportRow.vue new file mode 100644 index 000000000..cc81ae018 --- /dev/null +++ b/webapp/components/features/ReportRow/ReportRow.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/webapp/components/features/ReportsTable/ReportsTable.spec.js b/webapp/components/features/ReportsTable/ReportsTable.spec.js new file mode 100644 index 000000000..222ade83c --- /dev/null +++ b/webapp/components/features/ReportsTable/ReportsTable.spec.js @@ -0,0 +1,62 @@ +import { config, mount } from '@vue/test-utils' +import Vuex from 'vuex' +import ReportsTable from './ReportsTable.vue' +import { reports } from '~/components/features/ReportList/ReportList.story.js' + +const localVue = global.localVue + +config.stubs['client-only'] = '' +config.stubs['nuxt-link'] = '' + +describe('ReportsTable', () => { + let propsData, mocks, getters, wrapper, reportsTable + + beforeEach(() => { + propsData = {} + mocks = { + $t: jest.fn(string => string), + } + getters = { + 'auth/user': () => { + return { slug: 'awesome-user' } + }, + 'auth/isModerator': () => true, + } + }) + + describe('mount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + getters, + }) + return mount(ReportsTable, { propsData, mocks, localVue, store }) + } + + describe('given no reports', () => { + beforeEach(() => { + propsData = { ...propsData, reports: [] } + wrapper = Wrapper() + }) + + it('shows a placeholder', () => { + expect(wrapper.text()).toContain('moderation.reports.empty') + }) + }) + + describe('given reports', () => { + beforeEach(() => { + propsData = { ...propsData, reports } + wrapper = Wrapper() + reportsTable = wrapper.find('.ds-table') + }) + + it('renders a table', () => { + expect(reportsTable.exists()).toBe(true) + }) + + it('renders at least one ReportRow component', () => { + expect(wrapper.find('.report-row').exists()).toBe(true) + }) + }) + }) +}) diff --git a/webapp/components/features/ReportsTable/ReportsTable.story.js b/webapp/components/features/ReportsTable/ReportsTable.story.js new file mode 100644 index 000000000..1326b6c37 --- /dev/null +++ b/webapp/components/features/ReportsTable/ReportsTable.story.js @@ -0,0 +1,28 @@ +import { storiesOf } from '@storybook/vue' +import { withA11y } from '@storybook/addon-a11y' +import { action } from '@storybook/addon-actions' +import ReportsTable from '~/components/features/ReportsTable/ReportsTable' +import helpers from '~/storybook/helpers' +import { reports } from '~/components/features/ReportList/ReportList.story.js' + +helpers.init() + +storiesOf('ReportsTable', module) + .addDecorator(withA11y) + .addDecorator(helpers.layout) + .add('with reports', () => ({ + components: { ReportsTable }, + store: helpers.store, + data: () => ({ + reports, + }), + methods: { + confirm: action('confirm'), + }, + template: ``, + })) + .add('without reports', () => ({ + components: { ReportsTable }, + store: helpers.store, + template: ``, + })) diff --git a/webapp/components/features/ReportsTable/ReportsTable.vue b/webapp/components/features/ReportsTable/ReportsTable.vue new file mode 100644 index 000000000..512c04ea8 --- /dev/null +++ b/webapp/components/features/ReportsTable/ReportsTable.vue @@ -0,0 +1,50 @@ + + + diff --git a/webapp/components/utils/NormalizeEmail.js b/webapp/components/utils/NormalizeEmail.js new file mode 100644 index 000000000..f79c5118b --- /dev/null +++ b/webapp/components/utils/NormalizeEmail.js @@ -0,0 +1,11 @@ +import { normalizeEmail } from 'validator' + +export default email => + normalizeEmail(email, { + // gmail_remove_dots: false, default + gmail_remove_subaddress: false, + // gmail_convert_googlemaildotcom: true, default + outlookdotcom_remove_subaddress: false, + yahoo_remove_subaddress: false, + icloud_remove_subaddress: false, + }) diff --git a/webapp/constants/modals.js b/webapp/constants/modals.js index f1d08fdc3..6011b8b88 100644 --- a/webapp/constants/modals.js +++ b/webapp/constants/modals.js @@ -1,4 +1,4 @@ -// this list equals to enums in GraphQL schema file "backend/src/schema/types/type/REPORTED.gql" +// this list equals to enums in GraphQL schema file "backend/src/schema/types/type/FILED.gql" export const valuesReasonCategoryOptions = [ 'discrimination_etc', 'pornographic_content_links', diff --git a/webapp/graphql/Moderation.js b/webapp/graphql/Moderation.js index 1a5bdb367..df9d17870 100644 --- a/webapp/graphql/Moderation.js +++ b/webapp/graphql/Moderation.js @@ -1,80 +1,95 @@ import gql from 'graphql-tag' -export const reportListQuery = () => { - // no limit vor the moment like before: "reports(first: 20, orderBy: createdAt_desc)" +export const reportsListQuery = () => { + // no limit for the moment like before: "reports(first: 20, orderBy: createdAt_desc)" return gql` query { reports(orderBy: createdAt_desc) { + id createdAt - reasonCategory - reasonDescription - type - submitter { - id - slug - name - disabled - deleted + updatedAt + disable + closed + reviewed { + createdAt + updatedAt + disable + moderator { + id + slug + name + followedByCount + contributionsCount + commentedCount + } } - user { - id - slug - name - disabled - deleted - disabledBy { + resource { + __typename + ... on User { id slug name disabled deleted + followedByCount + contributionsCount + commentedCount } - } - comment { - id - contentExcerpt - author { + ... on Comment { id - slug - name + contentExcerpt disabled deleted + author { + id + slug + name + disabled + deleted + followedByCount + contributionsCount + commentedCount + } + post { + id + slug + title + disabled + deleted + } } - post { + ... on Post { id slug title disabled deleted - } - disabledBy { - id - slug - name - disabled - deleted + author { + id + slug + name + disabled + deleted + followedByCount + contributionsCount + commentedCount + } } } - post { - id - slug - title - disabled - deleted - author { - id - slug - name - disabled - deleted - } - disabledBy { + filed { + submitter { id slug name disabled deleted + followedByCount + contributionsCount + commentedCount } + createdAt + reasonCategory + reasonDescription } } } @@ -84,12 +99,23 @@ export const reportListQuery = () => { export const reportMutation = () => { return gql` mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report( + fileReport( resourceId: $resourceId reasonCategory: $reasonCategory reasonDescription: $reasonDescription ) { - type + id + } + } + ` +} + +export const reviewMutation = () => { + return gql` + mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) { + review(resourceId: $resourceId, disable: $disable, closed: $closed) { + disable + closed } } ` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 9dd8f57d6..d0008f102 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -184,7 +184,7 @@ "projects": "Projekte", "invites": "Einladungen", "follows": "Folgen", - "shouts": "Zurufe" + "shouts": "Empfehlungen" }, "organizations": { "name": "Organisationen" @@ -316,12 +316,67 @@ "reports": { "empty": "Glückwunsch, es gibt nichts zu moderieren.", "name": "Meldungen", - "reporter": "gemeldet von", - "submitter": "gemeldet von", - "disabledBy": "deaktiviert von", + "status": "Aktueller Status", + "content": "Inhalt", + "author": "Autor", + "decision": "Entscheidung", + "enabled": "Entsperrt", + "disabled": "Gesperrt", + "decided": "Entschieden", + "noDecision": "Keine Entscheidung!", + "decideButton": "Bestätige", + "DecisionSuccess": "Erfolgreich entschieden!", + "enabledBy": "Entsperrt von", + "disabledBy": "Gesperrt von", + "previousDecision": "Vorherige Entscheidung:", + "enabledAt": "Entsperrt am", + "disabledAt": "Gesperrt am", "reasonCategory": "Kategorie", "reasonDescription": "Beschreibung", - "createdAt": "Datum" + "submitter": "Gemeldet von", + "numberOfUsers": "{count} Nutzern", + "filterLabel": { + "all": "Alle", + "unreviewed": "Nicht bearbeitet", + "reviewed": "Bearbeitet", + "closed": "Abgeschlossen" + }, + "reportedOn": "Datum", + "moreDetails": "Details öffnen", + "decideModal": { + "submit": "Bestätige Entscheidung", + "cancel": "Abbruch", + "User": { + "disable": { + "title": "Sperre den Benutzer abschließend", + "message": "Möchtest du den Benutzer \"{name}\" wirklich gesperrt lassen?" + }, + "enable": { + "title": "Entsperre den Benutzer abschließend", + "message": "Möchtest du den Benutzer \"{name}\" wirklich entsperrt lassen?" + } + }, + "Post": { + "disable": { + "title": "Sperre den Beitrag abschließend", + "message": "Möchtest du den Beitrag \"{name}\" wirklich gesperrt lassen?" + }, + "enable": { + "title": "Entsperre den Beitrag abschließend", + "message": "Möchtest du den Beitrag \"{name}\" wirklich entsperrt lassen?" + } + }, + "Comment": { + "disable": { + "title": "Sperre den Kommentar abschließend", + "message": "Möchtest du den Kommentar \"{name}\" wirklich gesperrt lassen?" + }, + "enable": { + "title": "Entsperre den Kommentar abschließend", + "message": "Möchtest du den Kommentar \"{name}\" wirklich entsperrt lassen?" + } + } + } } }, "disable": { @@ -347,6 +402,7 @@ "report": { "submit": "Meldung senden", "cancel": "Abbrechen", + "success": "Vielen Dank für diese Meldung!", "user": { "title": "Nutzer melden", "type": "Nutzer", @@ -365,7 +421,6 @@ "message": "Bist Du sicher, dass Du den Kommentar von „{name}<\/b>“ melden möchtest?", "error": "Du hast den Kommentar bereits gemeldet!" }, - "success": "Vielen Dank für diese Meldung!", "reason": { "category": { "label": "Wähle eine Kategorie:", @@ -735,6 +790,10 @@ "title": "Fehler und Rückmeldungen", "description": "Wir sind sehr bemüht, unser Netzwerk und unsere Daten sicher und abrufbar zu erhalten. Jede neue Version der Software durchläuft sowohl automatisierte als auch manuelle Tests. Es können jedoch unvorhergesehene Fehler auftreten. Deshalb sind wir dankbar für jeden gemeldeten Fehler. Du kannst gerne jeden von Dir entdeckten Fehler dem Support\/der Hilfe-Assistenz mitteilen: support@human-connection.org" }, + "no-commercial-use" : { + "title": "Keine kommerzielle Nutzung", + "description": "Die Nutzung des Human Connection Netzwerkes ist nicht gestattet für kommerzielle Nutzung. Darunter fällt unter anderem das Bewerben von Produkten mit kommerzieller Absicht, das Einstellen von Affiliate-Links, direkter Aufruf zu Spenden oder finanzieller Unterstützung für Zwecke, die steuerlich nicht als gemeinnützig anerkannt sind." + }, "help-and-questions": { "title": "Hilfe und Fragen", "description": "Für Hilfe und Fragen haben wir Dir eine umfassende Sammlung an häufig gestellten Fragen und Antworten (FAQ) zusammengestellt. Du findest diese hier: https:\/\/support.human-connection.org\/kb\/ <\/a>" @@ -749,4 +808,4 @@ "donate-now": "Jetzt spenden", "amount-of-total": "{amount} von {total} € erreicht" } -} \ No newline at end of file +} diff --git a/webapp/locales/en.json b/webapp/locales/en.json index c34bb27d0..d1b815668 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -482,11 +482,67 @@ "reports": { "empty": "Congratulations, nothing to moderate.", "name": "Reports", + "status": "Current status", + "content": "Content", + "author": "Author", + "decision": "Decision", + "enabled": "Enabled", + "disabled": "Disabled", + "decided": "Decided", + "noDecision": "No decision!", + "decideButton": "Confirm", + "DecisionSuccess": "Decided successfully!", + "enabledBy": "Enabled by", + "disabledBy": "Disabled by", + "previousDecision": "Previous decision:", + "enabledAt": "Enabled at", + "disabledAt": "Disabled at", "reasonCategory": "Category", "reasonDescription": "Description", - "createdAt": "Date", "submitter": "Reported by", - "disabledBy": "Disabled by" + "numberOfUsers": "{count} users", + "filterLabel": { + "all": "All", + "unreviewed": "Unreviewed", + "reviewed": "Reviewed", + "closed": "Closed" + }, + "reportedOn": "Date", + "moreDetails": "View Details", + "decideModal": { + "submit": "Confirm decision", + "cancel": "Cancel", + "User": { + "disable": { + "title": "Finally Disable User", + "message": "Do you really want to let the user \"{name}\" stay disabled?" + }, + "enable": { + "title": "Finally Enable User", + "message": "Do you really want to let the user \"{name}\" stay enabled?" + } + }, + "Post": { + "disable": { + "title": "Finally Disable Post", + "message": "Do you really want to let the post \"{name}\" stay disabled?" + }, + "enable": { + "title": "Finally Enable Post", + "message": "Do you really want to let the post \"{name}\" stay enabled?" + } + }, + "Comment": { + "disable": { + "title": "Finally Disable Comment", + "message": "Do you really want to let the comment \"{name}\" stay disabled?" + }, + "enable": { + "title": "Finally Enable Comment", + "message": "Do you really want to let the comment \"{name}\" stay enabled?" + } + } + } } }, "disable": { @@ -727,6 +783,10 @@ "title": "Errors and Feedback", "description": "We make every effort to keep our network and data secure and available. Each new release of the software goes through both automated and manual testing. However, unforeseen errors may occur. Therefore, we are grateful for any reported bugs. You are welcome to report any bugs you discover by emailing Support at support@human-connection.org" }, + "no-commercial-use" : { + "title": "No Commercial Use", + "description": "The use of the Human Connection Network is not permitted for commercial purposes. This includes, but is not limited to, advertising products with commercial intent, posting affiliate links, directly soliciting donations, or providing financial support for purposes that are not recognized as charitable for tax purposes." + }, "help-and-questions" : { "title": "Help and Questions", "description": "For help and questions we have compiled a comprehensive collection of frequently asked questions and answers (FAQ) for you. You can find them here: https://support.human-connection.org/kb/ " diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index ecad7851b..c8a2952b4 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -11,7 +11,7 @@ "no-account": "Ainda não tem uma conta?", "register": "Cadastrar-se", "moreInfoURL": "https:\/\/human-connection.org\/en\/", - "moreInfoHint": "", + "moreInfoHint": "para a página de apresentação", "success": "Você está conectado!", "failure": "Endereço de e-mail ou senha incorretos." }, @@ -125,15 +125,15 @@ "name": "Fornecedores de terceiros", "info-description": "Se você concordar, as publicações da seguinte lista de provedores incluirão automaticamente código de terceiros de outros provedores (terceiros) na forma de vídeos, imagens ou texto incorporados.", "status": { - "description": "", + "description": "Como padrão para você, o código incorporado de provedores de terceiros é", "disabled": { - "off": "", - "on": "" + "off": "não exibido inicialmente", + "on": "exibido imediatamente" }, "change": { - "question": "", - "allow": "", - "deny": "" + "question": "O código-fonte incorporado de terceiros deve sempre ser exibido para você?", + "allow": "Certeza", + "deny": "Não, obrigado" } } }, @@ -158,13 +158,13 @@ "columns": { "name": "Nome", "slug": "Slug", - "unblock": "" + "unblock": "Desbloquear" }, "empty": "Até agora, você não bloqueou ninguém.", "how-to": "Você pode bloquear outros usuários em suas páginas de perfil através do menu de conteúdo.", "block": "Bloquear usuário", "unblock": "Desbloquear usuário", - "unblocked": "" + "unblocked": "{name} está desbloqueado novamente" }, "privacy": { "name": "Privacidade", @@ -261,10 +261,10 @@ "menu": { "edit": "Editar publicação", "delete": "Excluir publicação", - "pin": "", - "pinnedSuccessfully": "", - "unpin": "", - "unpinnedSuccessfully": "" + "pin": "Fixar publicação", + "pinnedSuccessfully": "Publicação fixada com sucesso!", + "unpin": "Desafixar publicação", + "unpinnedSuccessfully": "Publicação desafixada com sucesso!" }, "comment": { "submit": "Commentar", @@ -299,7 +299,7 @@ "validations": { "email": "deve ser um endereço de e-mail válido", "url": "deve ser uma URL válida", - "categories": "" + "categories": "devem ser seleccionadas, no mínimo uma e, no máximo três categorias" } }, "actions": { @@ -430,7 +430,7 @@ "teaserImage": { "cropperConfirm": "Confirmar" }, - "languageSelectText": "" + "languageSelectText": "Selecionar Idioma" }, "comment": { "edit": "Editar Comentário", @@ -745,8 +745,8 @@ } }, "donations": { - "donations-for": "", - "donate-now": "", - "amount-of-total": "" + "donations-for": "Doações para", + "donate-now": "Doe agora", + "amount-of-total": "{amount} dos {total} € foram coletados" } } \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index a4322aebd..8826a1a28 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -58,7 +58,7 @@ }, "dependencies": { "@human-connection/styleguide": "0.5.21", - "@nuxtjs/apollo": "^4.0.0-rc17", + "@nuxtjs/apollo": "^4.0.0-rc18", "@nuxtjs/axios": "~5.8.0", "@nuxtjs/dotenv": "~1.4.1", "@nuxtjs/pwa": "^3.0.0-beta.19", @@ -70,7 +70,7 @@ "cookie-universal-nuxt": "~2.0.19", "cropperjs": "^1.5.5", "cross-env": "~6.0.3", - "date-fns": "2.7.0", + "date-fns": "2.8.1", "express": "~4.17.1", "graphql": "~14.5.8", "jsonwebtoken": "~8.5.1", @@ -83,7 +83,7 @@ "string-hash": "^1.1.3", "tippy.js": "^4.3.5", "tiptap": "~1.26.3", - "tiptap-extensions": "~1.28.4", + "tiptap-extensions": "~1.28.5", "trunc-html": "^1.1.2", "v-tooltip": "~2.0.2", "validator": "^12.1.0", @@ -100,13 +100,13 @@ "@babel/core": "~7.7.4", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/preset-env": "~7.7.4", - "@storybook/addon-a11y": "^5.2.6", - "@storybook/addon-actions": "^5.2.6", - "@storybook/addon-notes": "^5.2.5", - "@storybook/vue": "~5.2.6", - "@vue/cli-shared-utils": "~4.0.5", + "@storybook/addon-a11y": "^5.2.8", + "@storybook/addon-actions": "^5.2.8", + "@storybook/addon-notes": "^5.2.8", + "@storybook/vue": "~5.2.8", + "@vue/cli-shared-utils": "~4.1.1", "@vue/eslint-config-prettier": "~6.0.0", - "@vue/server-test-utils": "~1.0.0-beta.29", + "@vue/server-test-utils": "~1.0.0-beta.30", "@vue/test-utils": "~1.0.0-beta.29", "async-validator": "^3.2.2", "babel-core": "~7.0.0-bridge.0", @@ -116,13 +116,13 @@ "babel-plugin-require-context-hook": "^1.0.0", "babel-preset-vue": "~2.0.2", "core-js": "~2.6.10", - "css-loader": "~3.2.0", - "eslint": "~6.7.1", + "css-loader": "~3.2.1", + "eslint": "~6.7.2", "eslint-config-prettier": "~6.7.0", "eslint-config-standard": "~14.1.0", "eslint-loader": "~3.0.2", "eslint-plugin-import": "~2.18.2", - "eslint-plugin-jest": "~23.0.4", + "eslint-plugin-jest": "~23.1.1", "eslint-plugin-node": "~10.0.0", "eslint-plugin-prettier": "~3.1.1", "eslint-plugin-promise": "~4.2.1", diff --git a/webapp/pages/admin/users.vue b/webapp/pages/admin/users.vue index 67594180d..6a7bf8adb 100644 --- a/webapp/pages/admin/users.vue +++ b/webapp/pages/admin/users.vue @@ -67,7 +67,8 @@ diff --git a/webapp/pages/notifications/index.spec.js b/webapp/pages/notifications/index.spec.js index 61af56f63..66f68aac7 100644 --- a/webapp/pages/notifications/index.spec.js +++ b/webapp/pages/notifications/index.spec.js @@ -71,7 +71,7 @@ describe('PostIndex', () => { } }) - describe('filterNotifications', () => { + describe('filter', () => { beforeEach(() => { propsData.filterOptions = [ { label: 'All', value: null }, @@ -79,7 +79,7 @@ describe('PostIndex', () => { { label: 'Unread', value: false }, ] wrapper = Wrapper() - wrapper.find(DropdownFilter).vm.$emit('filterNotifications', propsData.filterOptions[1]) + wrapper.find(DropdownFilter).vm.$emit('filter', propsData.filterOptions[1]) }) it('sets `notificationRead` to value of received option', () => { diff --git a/webapp/pages/notifications/index.vue b/webapp/pages/notifications/index.vue index f18d5afd2..aa002cec7 100644 --- a/webapp/pages/notifications/index.vue +++ b/webapp/pages/notifications/index.vue @@ -6,11 +6,7 @@ - + @@ -60,7 +56,7 @@ export default { }, }, methods: { - filterNotifications(option) { + filter(option) { this.notificationRead = option.value this.selected = option.label this.$apollo.queries.notifications.refresh() diff --git a/webapp/pages/settings/my-email-address/index.vue b/webapp/pages/settings/my-email-address/index.vue index 5a84f3795..8b3889b0d 100644 --- a/webapp/pages/settings/my-email-address/index.vue +++ b/webapp/pages/settings/my-email-address/index.vue @@ -32,7 +32,7 @@ import { mapGetters } from 'vuex' import { AddEmailAddressMutation } from '~/graphql/EmailAddress.js' import { SweetalertIcon } from 'vue-sweetalert-icons' -import { normalizeEmail } from 'validator' +import normalizeEmail from '~/components/utils/NormalizeEmail' export default { components: { diff --git a/webapp/pages/terms-and-conditions.vue b/webapp/pages/terms-and-conditions.vue index 10d805710..81e55f6aa 100644 --- a/webapp/pages/terms-and-conditions.vue +++ b/webapp/pages/terms-and-conditions.vue @@ -34,6 +34,7 @@ export default { 'code-of-conduct', 'moderation', 'errors-and-feedback', + 'no-commercial-use', 'help-and-questions', 'addition', ], diff --git a/webapp/yarn.lock b/webapp/yarn.lock index ba93f8f2a..319d07178 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -106,13 +106,6 @@ lodash "^4.17.13" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" - integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== - dependencies: - "@babel/types" "^7.0.0" - "@babel/helper-annotate-as-pure@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.4.tgz#bb3faf1e74b74bd547e867e48f551fa6b098b6ce" @@ -161,6 +154,18 @@ "@babel/helper-replace-supers" "^7.5.5" "@babel/helper-split-export-declaration" "^7.4.4" +"@babel/helper-create-class-features-plugin@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.4.tgz#fce60939fd50618610942320a8d951b3b639da2d" + integrity sha512-l+OnKACG4uiDHQ/aJT8dwpR+LhCJALxL0mJ6nzjB25e5IPwqV1VOsY7ah6UB1DG+VOXAIMtuC54rFJGiHkxjgA== + dependencies: + "@babel/helper-function-name" "^7.7.4" + "@babel/helper-member-expression-to-functions" "^7.7.4" + "@babel/helper-optimise-call-expression" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.7.4" + "@babel/helper-split-export-declaration" "^7.7.4" + "@babel/helper-create-regexp-features-plugin@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.4.tgz#6d5762359fd34f4da1500e4cff9955b5299aaf59" @@ -379,7 +384,7 @@ "@babel/helper-remap-async-to-generator" "^7.7.4" "@babel/plugin-syntax-async-generators" "^7.7.4" -"@babel/plugin-proposal-class-properties@^7.3.3", "@babel/plugin-proposal-class-properties@^7.5.5": +"@babel/plugin-proposal-class-properties@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4" integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A== @@ -387,6 +392,14 @@ "@babel/helper-create-class-features-plugin" "^7.5.5" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-proposal-class-properties@^7.7.0": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz#2f964f0cb18b948450362742e33e15211e77c2ba" + integrity sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-decorators@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.6.0.tgz#6659d2572a17d70abd68123e89a12a43d90aa30c" @@ -412,7 +425,7 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.7.4" -"@babel/plugin-proposal-object-rest-spread@^7.3.2", "@babel/plugin-proposal-object-rest-spread@^7.7.4": +"@babel/plugin-proposal-object-rest-spread@^7.6.2", "@babel/plugin-proposal-object-rest-spread@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.7.4.tgz#cc57849894a5c774214178c8ab64f6334ec8af71" integrity sha512-rnpnZR3/iWKmiQyJ3LKJpSwLDcX/nSXhdLk4Aq/tXOApIvyu7qoabrige0ylsAJffaUC51WiBu209Q0U+86OWQ== @@ -677,12 +690,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-react-constant-elements@^7.2.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.5.0.tgz#4d6ae4033bc38f8a65dfca2b6235c44522a422fc" - integrity sha512-c5Ba8cpybZFp1Izkf2sWGuNjOxoQ32tFgBvvYvwGhi4+9f6vGiSK9Gex4uVuO/Va6YJFu41aAh1MzMjUWkp0IQ== +"@babel/plugin-transform-react-constant-elements@^7.6.3": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.7.4.tgz#499cf732a21ffd62cc4b0016e27c3906097f8982" + integrity sha512-U6XkHZ8RnmeEb8jBUOpeo6oFka5RhLgxAVvK4/fBbwoYlsHQYLb8I37ymTPDVsrWjqb94+hueuWQA/1OAA4rAQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-annotate-as-pure" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-regenerator@^7.7.4": @@ -754,7 +767,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/preset-env@^7.4.5", "@babel/preset-env@^7.6.3", "@babel/preset-env@~7.7.4": +"@babel/preset-env@^7.6.3", "@babel/preset-env@^7.7.1", "@babel/preset-env@~7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.7.4.tgz#ccaf309ae8d1ee2409c85a4e2b5e280ceee830f8" integrity sha512-Dg+ciGJjwvC1NIe/DGblMbcGq1HOtKbw8RLl4nIjlfcILKEOkWT/vRqPpumswABEBVudii6dnVwrBtzD7ibm4g== @@ -1549,14 +1562,14 @@ webpack-node-externals "^1.7.2" webpackbar "^4.0.0" -"@nuxtjs/apollo@^4.0.0-rc17": - version "4.0.0-rc17" - resolved "https://registry.yarnpkg.com/@nuxtjs/apollo/-/apollo-4.0.0-rc17.tgz#089ae5bdd70021d2a05076d1a5dfebdc5700cbe2" - integrity sha512-HCtbfvfO2H1HQEKNmPczZfroKPt4SYPyuY6XEMbYW6S16SGr95644Ne7/wo7BW4OSXybpr/HYXKYceEGga9Z2g== +"@nuxtjs/apollo@^4.0.0-rc18": + version "4.0.0-rc18" + resolved "https://registry.yarnpkg.com/@nuxtjs/apollo/-/apollo-4.0.0-rc18.tgz#0069cae64f414ed879d20de00881986dca6bb26c" + integrity sha512-DTwRw9XLJKyphZiVwtKn4hE6Vfn6BlxEDWFBMTXpKE3XUKpg5+Qcgr8GstkiKtWbOuNQi660KdZReJ48R8bxgQ== dependencies: cross-fetch "^3.0.4" universal-cookie "^4.0.2" - vue-apollo "^3.0.0" + vue-apollo "^3.0.1" vue-cli-plugin-apollo "^0.21.3" webpack-node-externals "^1.7.2" @@ -2043,17 +2056,17 @@ source-map-support "^0.5.7" tslib "^1.9.3" -"@storybook/addon-a11y@^5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-5.2.6.tgz#8aa603cac1eaa92ef53c51b842e502101dc5d074" - integrity sha512-LlY5TaZNDxl5MP8u2qCJmg8GhvbtYGXmWJv0GMof0cw1fuLMEBHKAa5vK2oWQdPjGpoSAYN80dqn3TyCqsoGnQ== +"@storybook/addon-a11y@^5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-5.2.8.tgz#30ca043dd376711eed9dd4443410d70754050178" + integrity sha512-rIujvSAmXhivMecdTAqIwWQ0PARLBmMBKqdLtCfkfwtRjfASJxEFGWRmRJramx37vIDYWS5r7uHfUpZbYSn6Fg== dependencies: - "@storybook/addons" "5.2.6" - "@storybook/api" "5.2.6" - "@storybook/client-logger" "5.2.6" - "@storybook/components" "5.2.6" - "@storybook/core-events" "5.2.6" - "@storybook/theming" "5.2.6" + "@storybook/addons" "5.2.8" + "@storybook/api" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/components" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/theming" "5.2.8" axe-core "^3.3.2" common-tags "^1.8.0" core-js "^3.0.1" @@ -2066,17 +2079,17 @@ redux "^4.0.1" util-deprecate "^1.0.2" -"@storybook/addon-actions@^5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.2.6.tgz#4fe411fc3bdb1d44058f23fbc8eb8d1bac29d521" - integrity sha512-CwTJPqe3NcEU7oqS5KoiCX9FXYmI2Dyp1Sh6r90JmXZ8B49ZXm6BDLX0gS3TooD6/AcdU8xdBcSvN0CkxQ5QGA== +"@storybook/addon-actions@^5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.2.8.tgz#f63c6e1afb59e94ca1ebc776b7cad9b815e7419e" + integrity sha512-hadk+UaU6upOW0g447RfLRrnXRgE2rjRVk5sT8mVxBMj032NnwUd7ie/BZwy1yg5B8oFtpkgQYwqhPtoO2xBaQ== dependencies: - "@storybook/addons" "5.2.6" - "@storybook/api" "5.2.6" - "@storybook/client-api" "5.2.6" - "@storybook/components" "5.2.6" - "@storybook/core-events" "5.2.6" - "@storybook/theming" "5.2.6" + "@storybook/addons" "5.2.8" + "@storybook/api" "5.2.8" + "@storybook/client-api" "5.2.8" + "@storybook/components" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/theming" "5.2.8" core-js "^3.0.1" fast-deep-equal "^2.0.1" global "^4.3.2" @@ -2086,18 +2099,18 @@ react-inspector "^3.0.2" uuid "^3.3.2" -"@storybook/addon-notes@^5.2.5": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/addon-notes/-/addon-notes-5.2.6.tgz#bf74ff4f8018e315a4c07c3d5e90cd9154ce6e8e" - integrity sha512-CfWOkoPFI1ZAWQYnwFVqGmeCeXnVQGoFyDSVc3NcIFF1lsk2aagGV+ifJMJuDTXIKu0FClKpvMcENWt+bBpA+w== +"@storybook/addon-notes@^5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/addon-notes/-/addon-notes-5.2.8.tgz#9a4c90696aae64626e892b1a0f647ae79bab56a7" + integrity sha512-6we9WUD4cQzGhy9yej7ob+5SWk5OZ2BC1PWEcZOR/GcgYSWNk8cNNnpqAlFqB+DAQk5yawZ41MhQom0NT6LWOw== dependencies: - "@storybook/addons" "5.2.6" - "@storybook/api" "5.2.6" - "@storybook/client-logger" "5.2.6" - "@storybook/components" "5.2.6" - "@storybook/core-events" "5.2.6" - "@storybook/router" "5.2.6" - "@storybook/theming" "5.2.6" + "@storybook/addons" "5.2.8" + "@storybook/api" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/components" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/router" "5.2.8" + "@storybook/theming" "5.2.8" core-js "^3.0.1" global "^4.3.2" markdown-to-jsx "^6.10.3" @@ -2130,6 +2143,19 @@ global "^4.3.2" util-deprecate "^1.0.2" +"@storybook/addons@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.2.8.tgz#f8bf8bd555b7a69fb1e9a52ab8cdb96384d931ff" + integrity sha512-yAo1N5z/45bNIQP8SD+HVTr7X898bYAtz1EZBrQ6zD8bGamzA2Br06rOLL9xXw29eQhsaVnPlqgDwCS1sTC7aQ== + dependencies: + "@storybook/api" "5.2.8" + "@storybook/channels" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/core-events" "5.2.8" + core-js "^3.0.1" + global "^4.3.2" + util-deprecate "^1.0.2" + "@storybook/api@5.1.9": version "5.1.9" resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.1.9.tgz#eec5b2f775392ce0803930104c6ce14fa4931e8b" @@ -2176,6 +2202,29 @@ telejson "^3.0.2" util-deprecate "^1.0.2" +"@storybook/api@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.2.8.tgz#21f03df8041114eb929bd10b570a17f266568b7f" + integrity sha512-rFrPtTFDIPQoicLwq1AVsOvZNTUKnjD1w/NX1kKcyuWLL9BcOkU3YNLBlliGBg2JX/yS+fJKMyKk4NMzNBCZCg== + dependencies: + "@storybook/channels" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/router" "5.2.8" + "@storybook/theming" "5.2.8" + core-js "^3.0.1" + fast-deep-equal "^2.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + prop-types "^15.6.2" + react "^16.8.3" + semver "^6.0.0" + shallow-equal "^1.1.0" + store2 "^2.7.1" + telejson "^3.0.2" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@5.2.6": version "5.2.6" resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-5.2.6.tgz#60aaef0e80300c9812a571ca3ce0f28e2c404f04" @@ -2187,6 +2236,17 @@ global "^4.3.2" telejson "^3.0.2" +"@storybook/channel-postmessage@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-5.2.8.tgz#7a84869ce0fc270c3b5dcd7fa4ed798b6055816f" + integrity sha512-RS3iDW1kpfODN+kBq3youn+KtLqHslZ4m7mTlOL80BUHKb4YkrA1lVkzpy1kVMWBU523pyDVQUVXr+M8y3iVug== + dependencies: + "@storybook/channels" "5.2.8" + "@storybook/client-logger" "5.2.8" + core-js "^3.0.1" + global "^4.3.2" + telejson "^3.0.2" + "@storybook/channels@5.1.9": version "5.1.9" resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.1.9.tgz#003cfca0b9f1ba6cf47ce68304aedd71bdb55e74" @@ -2201,6 +2261,13 @@ dependencies: core-js "^3.0.1" +"@storybook/channels@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.2.8.tgz#79a99ad85dcacb688073c22340c5b7d16b801202" + integrity sha512-mFwQec27QSrqcl+IH0xA+4jfoEqC4m1G99LBHt/aTDjLZXclX1A470WqeZCp7Gx4OALpaPEVTaaaKPbiKz4C6w== + dependencies: + core-js "^3.0.1" + "@storybook/client-api@5.2.6": version "5.2.6" resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-5.2.6.tgz#5760cb4302d82ce9210a63f3f55b1e05f04759c1" @@ -2222,6 +2289,28 @@ qs "^6.6.0" util-deprecate "^1.0.2" +"@storybook/client-api@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-5.2.8.tgz#1de791f7888442287f848e5f544eb883c5edc0da" + integrity sha512-OCKhZ+2sS3ot0ZV48nD79BWVzvvdMjUFYl0073ps5q+1+TLic1AlNmH0Sb5/9NrYXNV86v3VrM2jUbGsKe1qyw== + dependencies: + "@storybook/addons" "5.2.8" + "@storybook/channel-postmessage" "5.2.8" + "@storybook/channels" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/router" "5.2.8" + common-tags "^1.8.0" + core-js "^3.0.1" + eventemitter3 "^4.0.0" + global "^4.3.2" + is-plain-object "^3.0.0" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + stable "^0.1.8" + util-deprecate "^1.0.2" + "@storybook/client-logger@5.1.9": version "5.1.9" resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.1.9.tgz#87e2f7578416269adeccd407584010bc353f14d3" @@ -2236,6 +2325,13 @@ dependencies: core-js "^3.0.1" +"@storybook/client-logger@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.2.8.tgz#5affe2f9dbbee374721fd2e8729116f5ac39c779" + integrity sha512-+oVSEJdeh7TQ1Bhanb3mCr7fc3Bug3+K79abZ28J45Ub5x4L/ZVClj1xMgUsJs30BZ5FB8vhdgH6TQb0NSxR4A== + dependencies: + core-js "^3.0.1" + "@storybook/components@5.1.9": version "5.1.9" resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.1.9.tgz#2a5258780fff07172d103287759946dbb4b13e2d" @@ -2285,6 +2381,31 @@ react-textarea-autosize "^7.1.0" simplebar-react "^1.0.0-alpha.6" +"@storybook/components@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.2.8.tgz#f5d4a06ba4ba8c700b2d962deae182105b72fb99" + integrity sha512-h9l/LAMaj+emUCOyY/+ETy/S3P0npwQU280J88uL4O9XJALJ72EKfyttBCvMLvpM50E+fAPeDzuYn0t5qzGGxg== + dependencies: + "@storybook/client-logger" "5.2.8" + "@storybook/theming" "5.2.8" + "@types/react-syntax-highlighter" "10.1.0" + "@types/react-textarea-autosize" "^4.3.3" + core-js "^3.0.1" + global "^4.3.2" + markdown-to-jsx "^6.9.1" + memoizerific "^1.11.3" + polished "^3.3.1" + popper.js "^1.14.7" + prop-types "^15.7.2" + react "^16.8.3" + react-dom "^16.8.3" + react-focus-lock "^1.18.3" + react-helmet-async "^1.0.2" + react-popper-tooltip "^2.8.3" + react-syntax-highlighter "^8.0.1" + react-textarea-autosize "^7.1.0" + simplebar-react "^1.0.0-alpha.6" + "@storybook/core-events@5.1.9": version "5.1.9" resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.1.9.tgz#441a6297e2ccfa743e15d1db1f4ac445b91f40d8" @@ -2299,25 +2420,32 @@ dependencies: core-js "^3.0.1" -"@storybook/core@5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-5.2.6.tgz#60c092607158d7d28db59f7e67da4f7e12703fb2" - integrity sha512-q7Ful7TCm9nmjgLsJFqIwVv395NlaOXgGajyaQCQlCKB2V+jgs7GDmdCNNdWAOue4eAsFU6wQSP9lWtq0yzK4w== +"@storybook/core-events@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.2.8.tgz#93fc458ea0820ff1409d268b0fe51abba200f5a4" + integrity sha512-NkQKC5doO/YL9gsO61bqaxgveKktkiJWZ3XyyhL1ZebgnO9wTlrU+i9b5aX73Myk1oxbicQw9KcwDGYk0qFuNQ== dependencies: - "@babel/plugin-proposal-class-properties" "^7.3.3" - "@babel/plugin-proposal-object-rest-spread" "^7.3.2" + core-js "^3.0.1" + +"@storybook/core@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-5.2.8.tgz#3f6ddbacc705c1893deb15582c3a0a1ecd882cd1" + integrity sha512-P1Xx4setLBESPgS5KgL7Jskf5Q6fRa3ApwPt+ocjDoSDGCvsV7cUEpAp09U65u+89e5K4nQxvaZouhknFQBc1A== + dependencies: + "@babel/plugin-proposal-class-properties" "^7.7.0" + "@babel/plugin-proposal-object-rest-spread" "^7.6.2" "@babel/plugin-syntax-dynamic-import" "^7.2.0" - "@babel/plugin-transform-react-constant-elements" "^7.2.0" - "@babel/preset-env" "^7.4.5" - "@storybook/addons" "5.2.6" - "@storybook/channel-postmessage" "5.2.6" - "@storybook/client-api" "5.2.6" - "@storybook/client-logger" "5.2.6" - "@storybook/core-events" "5.2.6" - "@storybook/node-logger" "5.2.6" - "@storybook/router" "5.2.6" - "@storybook/theming" "5.2.6" - "@storybook/ui" "5.2.6" + "@babel/plugin-transform-react-constant-elements" "^7.6.3" + "@babel/preset-env" "^7.7.1" + "@storybook/addons" "5.2.8" + "@storybook/channel-postmessage" "5.2.8" + "@storybook/client-api" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/node-logger" "5.2.8" + "@storybook/router" "5.2.8" + "@storybook/theming" "5.2.8" + "@storybook/ui" "5.2.8" airbnb-js-shims "^1 || ^2" ansi-to-html "^0.6.11" autoprefixer "^9.4.9" @@ -2373,10 +2501,10 @@ webpack-dev-middleware "^3.7.0" webpack-hot-middleware "^2.25.0" -"@storybook/node-logger@5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.2.6.tgz#e353aff14375bef9e922c217a0afb50f93e2ceb1" - integrity sha512-Z3mn9CUSiG7kR2OBoz4lNeoeBS094h5d9wufZSp5S+M47L6KEXmTgNcuePKj+t8Z8KT/Ph8B63bjChseKp3DNw== +"@storybook/node-logger@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.2.8.tgz#4a3df21d731014d54b9ca53d5b9a72dd350bb075" + integrity sha512-3TK5mx6VWbfJO+WUrqwPhTbTQ4qESTnwJY/02xPzOhvuC6tIG1QOxzi+Rq6rFlwxTpUuWh6iyDYnGIqFFQywkA== dependencies: chalk "^2.4.2" core-js "^3.0.1" @@ -2408,6 +2536,19 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-5.2.8.tgz#d7de2d401701857c033e28560c30e16512f7f72f" + integrity sha512-wnbyKESUMyv9fwo9W+n4Fev/jXylB8whpjtHrOttjguUOYX1zGSHdwNI66voPetbtVLxUeHyJteJwdyRDSirJg== + dependencies: + "@reach/router" "^1.2.1" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + "@storybook/theming@5.1.9": version "5.1.9" resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.1.9.tgz#c425f5867fae0db79e01112853b1808332a5f1a2" @@ -2444,19 +2585,37 @@ prop-types "^15.7.2" resolve-from "^5.0.0" -"@storybook/ui@5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-5.2.6.tgz#33df2f2e03d9cf81dc52928a0dc4db280ee8f56a" - integrity sha512-jT3PtpEsTqnESO0U8BotC+5P971Xqy0s2leSZcgU9PNe4Eb7NaxypSULOulPgPAx1JOmMipUBdK54PP/nyudkA== +"@storybook/theming@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.2.8.tgz#a4c9e0e9a5789c1aa71e4fcb7a8ee86efe3dadcf" + integrity sha512-rGb66GkXb0jNJMH8UQ3Ru4FL+m1x0+UdxM8a8HSE/qb1GMv2qOwjVETfAL6nVL9u6ZmrtbhHoero4f6xDwZdRg== dependencies: - "@storybook/addons" "5.2.6" - "@storybook/api" "5.2.6" - "@storybook/channels" "5.2.6" - "@storybook/client-logger" "5.2.6" - "@storybook/components" "5.2.6" - "@storybook/core-events" "5.2.6" - "@storybook/router" "5.2.6" - "@storybook/theming" "5.2.6" + "@emotion/core" "^10.0.14" + "@emotion/styled" "^10.0.14" + "@storybook/client-logger" "5.2.8" + common-tags "^1.8.0" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.14" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.3.1" + prop-types "^15.7.2" + resolve-from "^5.0.0" + +"@storybook/ui@5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-5.2.8.tgz#da8afca9eb29a40ef3ddc6a9f6e76d7a3344f2ef" + integrity sha512-7t1ARBfylhEsLmGsZBUCj1Wf1oAgCDDrf7fi+Fhdg5Rr16CMoBbe24Gv/mPYv01/pUDhGodxzltKGX5x0Hto2w== + dependencies: + "@storybook/addons" "5.2.8" + "@storybook/api" "5.2.8" + "@storybook/channels" "5.2.8" + "@storybook/client-logger" "5.2.8" + "@storybook/components" "5.2.8" + "@storybook/core-events" "5.2.8" + "@storybook/router" "5.2.8" + "@storybook/theming" "5.2.8" copy-to-clipboard "^3.0.8" core-js "^3.0.1" core-js-pure "^3.0.1" @@ -2483,13 +2642,13 @@ telejson "^3.0.2" util-deprecate "^1.0.2" -"@storybook/vue@~5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@storybook/vue/-/vue-5.2.6.tgz#1d79880c7e1a14e4deb66c5e438ed3326a3c7c47" - integrity sha512-jYoe+rHT1mKCJp8tcbE6ZPIhLidi5nD9z4XKzfHCZ4HsIWSl7dD+Q7QX1JeZYCCMf6VA27G40l4hUJHWEUyWxA== +"@storybook/vue@~5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@storybook/vue/-/vue-5.2.8.tgz#1e80a855e9d5258945d17399decaa5a0cfb75b61" + integrity sha512-d/Xzu5sNEvWHKml3Lorpue0s3yd7JdrHySsAia4QBwPum0/GiVbTRUnWtJC9Gi/iWXBkqfB/sejs8NIh/QHHZA== dependencies: - "@storybook/addons" "5.2.6" - "@storybook/core" "5.2.6" + "@storybook/addons" "5.2.8" + "@storybook/core" "5.2.8" "@types/webpack-env" "^1.13.9" common-tags "^1.8.0" core-js "^3.0.1" @@ -2568,6 +2727,13 @@ dependencies: "@types/node" "*" +"@types/cheerio@^0.22.10": + version "0.22.14" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.14.tgz#d150889891e7db892c6a0b16bd5583cc70b3fc44" + integrity sha512-SVtcP2fvPYrebTwpyqxjxb7K5v3ZOAdH409yAEWFPpZThCSGa1K2IFfx6Rg6ttvThCBQXP4fU9WF94sqLoiQGg== + dependencies: + "@types/node" "*" + "@types/connect@*": version "3.4.32" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" @@ -2961,10 +3127,10 @@ "@vue/babel-plugin-transform-vue-jsx" "^1.0.0" camelcase "^5.0.0" -"@vue/cli-shared-utils@~4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.0.5.tgz#dd263fa3b3a75c11cdc64376d4c45470fba6b270" - integrity sha512-NlNZ4Dx5QcP5uO5fCOLgkN2tbhNan5EcptPvXawW/md18cIpMlKbph6L6lEfJj8vrSvTUf2i/FyoFSh1rV53hw== +"@vue/cli-shared-utils@~4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.1.1.tgz#79e26b56fda185fda00e5787a8f4aac56757b123" + integrity sha512-nsxNW8Sy9y2yx/r9DqgZoYg/DoygvASIQl0XXG+imQUDWEXKmD6UZA6y5ANfStCljzZ/wd7WgWP+txmjy6exOw== dependencies: "@hapi/joi" "^15.0.1" chalk "^2.4.1" @@ -2975,9 +3141,10 @@ open "^6.3.0" ora "^3.4.0" request "^2.87.0" - request-promise-native "^1.0.7" + request-promise-native "^1.0.8" semver "^6.1.0" string.prototype.padstart "^3.0.0" + strip-ansi "^6.0.0" "@vue/component-compiler-utils@^3.0.0": version "3.0.0" @@ -3001,11 +3168,12 @@ dependencies: eslint-config-prettier "^6.0.0" -"@vue/server-test-utils@~1.0.0-beta.29": - version "1.0.0-beta.29" - resolved "https://registry.yarnpkg.com/@vue/server-test-utils/-/server-test-utils-1.0.0-beta.29.tgz#8a61a9900992b742cda7c143593db1c5e4a019bd" - integrity sha512-N6e2cixTnz7+mos0wL+R3LM/2op+f3ym1aSQYuNfksZzvX81lrQtjGr6Aszv0+1Ryms+WrqlL18Jb/MY8XyXaQ== +"@vue/server-test-utils@~1.0.0-beta.30": + version "1.0.0-beta.30" + resolved "https://registry.yarnpkg.com/@vue/server-test-utils/-/server-test-utils-1.0.0-beta.30.tgz#960a6383de81ba5ff763bdd6e961b9443e98fa82" + integrity sha512-H5vABB3uWbr+JlpN9pA7OpepYsHSmocbIHJQA00Xz0s4Qqmg4jX93GArO4KVC7zs/QS12w3mXfmMlYV8w7DwEg== dependencies: + "@types/cheerio" "^0.22.10" cheerio "^1.0.0-rc.2" "@vue/test-utils@~1.0.0-beta.29": @@ -3392,6 +3560,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -6137,23 +6310,23 @@ css-has-pseudo@^0.10.0: postcss "^7.0.6" postcss-selector-parser "^5.0.0-rc.4" -css-loader@^3.0.0, css-loader@^3.2.0, css-loader@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.0.tgz#bb570d89c194f763627fcf1f80059c6832d009b2" - integrity sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ== +css-loader@^3.0.0, css-loader@^3.2.0, css-loader@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.1.tgz#62849b45a414b7bde0bfba17325a026471040eae" + integrity sha512-q40kYdcBNzMvkIImCL2O+wk8dh+RGwPPV9Dfz3n7XtOYPXqe2Z6VgtvoxjkLHz02gmhepG9sOAJOUlx+3hHsBg== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.17" + postcss "^7.0.23" postcss-modules-extract-imports "^2.0.0" postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.1.0" + postcss-modules-scope "^2.1.1" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.0" - schema-utils "^2.0.0" + postcss-value-parser "^4.0.2" + schema-utils "^2.6.0" css-prefers-color-scheme@^3.1.1: version "3.1.1" @@ -6383,10 +6556,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.7.0.tgz#8271d943cc4636a1f27698f1b8d6a9f1ceb74026" - integrity sha512-wxYp2PGoUDN5ZEACc61aOtYFvSsJUylIvCjpjDOqM1UDaKIIuMJ9fAnMYFHV3TQaDpfTVxhwNK/GiCaHKuemTA== +date-fns@2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.8.1.tgz#2109362ccb6c87c3ca011e9e31f702bc09e4123b" + integrity sha512-EL/C8IHvYRwAHYgFRse4MGAPSqlJVlOrhVYZ75iQBKrnv+ZedmYsgwH3t+BCDuZDXpoo07+q9j4qgSSOa7irJg== date-fns@^1.27.2: version "1.30.1" @@ -7094,10 +7267,10 @@ eslint-plugin-import@~2.18.2: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~23.0.4: - version "23.0.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz#1ab81ffe3b16c5168efa72cbd4db14d335092aa0" - integrity sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw== +eslint-plugin-jest@~23.1.1: + version "23.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.1.1.tgz#1220ab53d5a4bf5c3c4cd07c0dabc6199d4064dd" + integrity sha512-2oPxHKNh4j1zmJ6GaCBuGcb8FVZU7YjFUOJzGOPnl9ic7VA/MGAskArLJiRIlnFUmi1EUxY+UiATAy8dv8s5JA== dependencies: "@typescript-eslint/experimental-utils" "^2.5.0" @@ -7165,10 +7338,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" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== -eslint@~6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.1.tgz#269ccccec3ef60ab32358a44d147ac209154b919" - integrity sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA== +eslint@~6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1" + integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -8578,10 +8751,10 @@ highlight.js@~9.12.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" integrity sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4= -highlight.js@~9.15.0: - version "9.15.6" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4" - integrity sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ== +highlight.js@~9.16.0: + version "9.16.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.16.2.tgz#68368d039ffe1c6211bcc07e483daf95de3e403e" + integrity sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw== hmac-drbg@^1.0.0: version "1.0.1" @@ -10623,13 +10796,13 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== -lowlight@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb" - integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w== +lowlight@1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.0.tgz#9b4fd00559985e40e11c916ccab14c7c0cf4320d" + integrity sha512-bFXLa+UO1eM3zieFAcNqf6rTQ1D5ERFv64/euQbbH/LT3U9XXwH6tOrgUAGWDsQ1QgN3ZhgOcv8p3/S+qKGdTQ== dependencies: fault "^1.0.2" - highlight.js "~9.15.0" + highlight.js "~9.16.0" lowlight@~1.9.1: version "1.9.2" @@ -10725,7 +10898,7 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-to-jsx@^6.10.3: +markdown-to-jsx@^6.10.3, markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: version "6.10.3" resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.10.3.tgz#7f0946684acd321125ff2de7fd258a9b9c7c40b7" integrity sha512-PSoUyLnW/xoW6RsxZrquSSz5eGEOTwa15H5eqp3enmrp8esmgDJmhzd6zmQ9tgAA9TxJzx1Hmf3incYU/IamoQ== @@ -10733,14 +10906,6 @@ markdown-to-jsx@^6.10.3: prop-types "^15.6.2" unquote "^1.1.0" -markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: - version "6.10.2" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.10.2.tgz#644f602b81d088f10aef1c3674874876146cf38b" - integrity sha512-eDCsRobOkbQ4PqCphrxNi/U8geA8DGf52dMP4BrrYsVFyQ2ILFnXIB5sRcIxnRK2nPl8k5hUYdRNRXLlQNYLYg== - dependencies: - prop-types "^15.6.2" - unquote "^1.1.0" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -11722,10 +11887,10 @@ ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -orderedmap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba" - integrity sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo= +orderedmap@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.1.0.tgz#dc41a147130b51e203e8523ea5ea57724af2b63d" + integrity sha512-abUlPCcmyI/17BWWoUWeAbnniTFUZuczP6iowD9XMBcUoD9jRtUO6w+KXkw64TDk+iHdyDfTAjIH7mTRzhXcaw== original@^1.0.0: version "1.0.2" @@ -12532,10 +12697,10 @@ postcss-modules-local-by-default@^3.0.2: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.0" -postcss-modules-scope@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" - integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== +postcss-modules-scope@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz#33d4fc946602eb5e9355c4165d68a10727689dba" + integrity sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ== dependencies: postcss "^7.0.6" postcss-selector-parser "^6.0.0" @@ -12823,7 +12988,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== @@ -12837,28 +13002,10 @@ postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: indexes-of "^1.0.1" uniq "^1.0.1" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.16" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.16.tgz#48f64f1b4b558cb8b52c88987724359acb010da2" - integrity sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.17: - version "7.0.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" - integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.18: - version "7.0.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233" - integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g== +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0.2, postcss@^7.0.23, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.23.tgz#9f9759fad661b15964f3cfc3140f66f1e05eadc1" + integrity sha512-hOlMf3ouRIFXD+j2VJecwssTwbvsPGJVMzupptg+85WA+i7MwyrydmQAgY3R+m0Bc0exunhbJmijy8u8+vufuQ== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -13019,58 +13166,66 @@ property-information@^5.0.1: dependencies: xtend "^4.0.1" -prosemirror-collab@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.1.2.tgz#622fdc52692a83045ba6914c01a0416ff35f646a" - integrity sha512-ew0p0XWlp3PYc4h20hOfix8UPSRaJMRHQQCMoIUzxoiBgGGij/N4pXIY+w/iw5JISgP8QYyOy5arOYnCzFJQlQ== +prosemirror-collab@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.2.2.tgz#8d2c0e82779cfef5d051154bd0836428bd6d9c4a" + integrity sha512-tBnHKMLgy5Qmx9MYVcLfs3pAyjtcqYYDd9kp3y+LSiQzkhMQDfZSV3NXWe4Gsly32adSef173BvObwfoSQL5MA== dependencies: prosemirror-state "^1.0.0" -prosemirror-commands@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.8.tgz#3b21e8b2f2e1c04ddb831b2d4055084b6d627bcc" - integrity sha512-P9QdkYYBHWsrJ1JztQuHgeZS7DPCcijQduOj9oxFiqK8Fm6eTsVHzU1IwiRBe+FlK7tyQaerhu/F5K8sqnZ1Cw== +prosemirror-commands@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.1.2.tgz#6868cabc9f9112fba94c805139473527774b0dea" + integrity sha512-JBa06kjgX67d9JVUVJbCkxwvSGtQnWAN/85nq9csOMS5Z9WZLEvVDtVvZranNlu8l/XNVBWrZxOOK+pB03eTfA== dependencies: prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-dropcursor@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.2.0.tgz#00b1bdb803ac28f5a68d7e0a16a47c9c11aab97d" - integrity sha512-D7JrvOgN32PmOgfimdDMKCuYp4tGyCulpsd39/Nzvn9A+tCJmM8XY1PB07zkr2vjrjF09WYD3Ifer7Z3pk/YRw== +prosemirror-dropcursor@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.3.2.tgz#28738c4ed7102e814d7a8a26d70018523fc7cd6d" + integrity sha512-4c94OUGyobGnwcQI70OXyMhE/9T4aTgjU+CHxkd5c7D+jH/J0mKM/lk+jneFVKt7+E4/M0D9HzRPifu8U28Thw== dependencies: prosemirror-state "^1.0.0" prosemirror-transform "^1.1.0" prosemirror-view "^1.1.0" -prosemirror-gapcursor@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.4.tgz#4ba663fb8511616e18ad222c904403cfbf6866dc" - integrity sha512-k021MtJibWs3NaJI6S9tCXfTZ/kaugFZBndHkkWx3Zfk0QDUO6JfVATpflxADN6DUkRwJ7qWyHlLDWu71hxHFQ== +prosemirror-gapcursor@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.1.2.tgz#a1400a86a51d4cccc065e68d5625a9fb5bc623e0" + integrity sha512-Z+eqk6RysZVxidGWN5aWoSTbn5bTHf1XZ+nQJVwUSdwdBVkfQMFdTHgfrXA8W5MhHHdNg/EEEYG3z3Zi/vE2QQ== dependencies: prosemirror-keymap "^1.0.0" prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" prosemirror-view "^1.0.0" -prosemirror-history@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.0.4.tgz#313a15489a78795321f3a37d53d8c68610c5adb6" - integrity sha512-Kk2UisC9EzYcsNv+ILiQJWpsu0rbT6+oAAkvseFUHnudtfkmYAJu1+Xp3F0xTTCVmQdSqSLVk8qydllXUUOU4Q== +prosemirror-history@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.1.2.tgz#3e8f11efbd316e98322028be67549df1f94fc6da" + integrity sha512-erhxYS5gm/6MiXP8jUoJBgc8IbaqjHDVPl9KGg5JrMZOSSOwHv85+4Fb0Q7sYtv2fYwAjOSw/kSA9vkxJ6wOwA== dependencies: prosemirror-state "^1.2.2" prosemirror-transform "^1.0.0" - rope-sequence "^1.2.0" + rope-sequence "^1.3.0" -prosemirror-inputrules@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.0.4.tgz#8ae84d9c147abb448b86063ff422e801c4b1658d" - integrity sha512-RhuBghqUgYWm8ai/P+k1lMl1ZGvt6Cs3Xeur8oN0L1Yy+Z5GmsTp3fT8RVl+vJeGkItEAxAit9Qh7yZxixX7rA== +prosemirror-inputrules@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.1.2.tgz#487e46c763e1212a4577397aba7706139084f012" + integrity sha512-Ja5Z3BWestlHYGvtSGqyvxMeB8QEuBjlHM8YnKtLGUXMDp965qdDV4goV8lJb17kIWHk7e7JNj6Catuoa3302g== dependencies: prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" +prosemirror-keymap@1.1.3, prosemirror-keymap@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.3.tgz#be22d6108df2521608e9216a87b1a810f0ed361e" + integrity sha512-PRA4NzkUMzV/NFf5pyQ6tmlIHiW/qjQ1kGWUlV2rF/dvlOxtpGpTEjIMhWgLuMf+HiDEFnUEP7uhYXu+t+491g== + dependencies: + prosemirror-state "^1.0.0" + w3c-keyname "^2.2.0" + prosemirror-keymap@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2" @@ -13079,64 +13234,56 @@ prosemirror-keymap@^1.0.0: prosemirror-state "^1.0.0" w3c-keyname "^1.1.8" -prosemirror-keymap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.2.tgz#e4e8b876a586ec6a170615fce19c4450f4075bef" - integrity sha512-aq3fBT3WMbwGNacUtMbS/mxd87hjJyjtUx5/h3q/P3FiVqHxmeA9snxQsZHYe0cWRziZePun8zw6kHFKLp/DAQ== +prosemirror-model@1.8.2, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.8.2.tgz#c74eaacb0bbfea49b59a6d89fef5516181666a56" + integrity sha512-piffokzW7opZVCjf/9YaoXvTC0g7zMRWKJib1hpphPfC+4x6ZXe5CiExgycoWZJe59VxxP7uHX8aFiwg2i9mUQ== dependencies: - prosemirror-state "^1.0.0" - w3c-keyname "^2.0.0" + orderedmap "^1.1.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.4.tgz#fadefad98ef9d71ca1749d18a82005313c978e4b" - integrity sha512-yxdpPh9Uv5vAOZvmbhg4fsGUK1oHuQs69iX7cFZ0A4Y+AyMMWRCNKUt21uv84HbXb4I180l4pJE8ibaH/SwYiw== - dependencies: - orderedmap "^1.0.0" - -prosemirror-schema-list@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.4.tgz#cbb11936e306aa45586af4279529ce61b52cfb6e" - integrity sha512-7Y0b6FIG6ATnCcDSLrZfU9yIfOG5Yad3DMNZ9W7GGfMSzdIl0aHExrsIUgviJZjovO2jtLJVbxWGjMR3OrTupA== +prosemirror-schema-list@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz#310809209094b03425da7f5c337105074913da6c" + integrity sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ== dependencies: prosemirror-model "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.2.4.tgz#aab932ebb33a0f27c256abce6fc20da8ca77ad5e" - integrity sha512-ViXpXond3BbSL12ENARQGq3Y8igwFMbTcy96xUNK8kfIcfQRlYlgYrBPXIkHC5+QZtbPrYlpuJ2+QyeSlSX9Cw== +prosemirror-state@1.3.2, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.3.2.tgz#1b910b0dc01c1f00926bb9ba1589f7b7ac0d658b" + integrity sha512-t/JqE3aR0SV9QrzFVkAXsQwsgrQBNs/BDbcFH20RssW0xauqNNdjTXxy/J/kM7F+0zYi6+BRmz7cMMQQFU3mwQ== dependencies: prosemirror-model "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-tables@^0.9.5: - version "0.9.5" - resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.9.5.tgz#94d9881a46051e6fff3c51edffafa346da084def" - integrity sha512-RlAF/D7OvnDCOL8B6Qt6KuBkb0w3SedTdrou7wH7Nn2ml7+M5xUalW/h1f7dMD3wjsU47/Cn8zTbEkCDIpIggw== +prosemirror-tables@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.0.0.tgz#ec3d0b11e638c6a92dd14ae816d0a2efd1719b70" + integrity sha512-zFw5Us4G5Vdq0yIj8GiqZOGA6ud5UKpMKElux9O0HrfmhkuGa1jf1PCpz2R5pmIQJv+tIM24H1mox/ODBAX37Q== dependencies: - prosemirror-keymap "^1.0.0" - prosemirror-model "^1.0.0" - prosemirror-state "^1.0.0" - prosemirror-transform "^1.0.0" - prosemirror-view "^1.0.0" + prosemirror-keymap "^1.1.2" + prosemirror-model "^1.8.1" + prosemirror-state "^1.3.1" + prosemirror-transform "^1.2.1" + prosemirror-view "^1.13.3" -prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.5.tgz#e50113d8004854eca1fa0711806bca53b1f1fad9" - integrity sha512-hJyRcwykLrAAj/ziNbVK1P/ensiszWJ2fNwNiDM8ZWYiMPwM4cAd2jptj/znxJfIvaj0S0psWL1+VhKwPNJDSQ== +prosemirror-transform@1.2.2, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.2.tgz#4439ae7e88ea1395d9beed6a4cd852d72b16ed2f" + integrity sha512-expO11jAsxaHk2RdZtzPsumc1bAAZi4UiXwTLQbftsdnIUWZE5Snyag595p1lx/B8QHUZ6tYWWOaOkzXKoJmYw== dependencies: prosemirror-model "^1.0.0" -prosemirror-utils@^0.9.6: +prosemirror-utils@0.9.6: version "0.9.6" resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973" integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA== -prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.11.7: - version "1.11.7" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.11.7.tgz#537020acab43e18d51e18717fb1f39466317ce21" - integrity sha512-RDll1mhrOQ5JPOsUZlISH3VP/4zyHm3RAuqh9Cg5HdpfSQow0nsx8xL5YQZ976UdhvwbkiKamLtxhSRKES9wsA== +prosemirror-view@1.13.4, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.13.4.tgz#01d873db7731e0aacc410a9038447d1b7536fd07" + integrity sha512-mtgWEK16uYQFk3kijRlkSpAmDuy7rxYuv0pgyEBDmLT1PCPY8380CoaYnP8znUT6BXIGlJ8oTveK3M50U+B0vw== dependencies: prosemirror-model "^1.1.0" prosemirror-state "^1.0.0" @@ -13858,19 +14005,19 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-promise-core@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" - integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== dependencies: - lodash "^4.17.11" + lodash "^4.17.15" -request-promise-native@^1.0.5, request-promise-native@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" - integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== +request-promise-native@^1.0.5, request-promise-native@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== dependencies: - request-promise-core "1.1.2" + request-promise-core "1.1.3" stealthy-require "^1.1.1" tough-cookie "^2.3.3" @@ -14027,10 +14174,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rope-sequence@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce" - integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4= +rope-sequence@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.2.tgz#a19e02d72991ca71feb6b5f8a91154e48e3c098b" + integrity sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg== rsvp@^4.8.4: version "4.8.4" @@ -14171,10 +14318,10 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.1.0, schema-utils@^2.2.0, schema-utils@^2.4.1, schema-utils@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f" - integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ== +schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.1.0, schema-utils@^2.2.0, schema-utils@^2.4.1, schema-utils@^2.5.0, schema-utils@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.1.tgz#eb78f0b945c7bcfa2082b3565e8db3548011dc4f" + integrity sha512-0WXHDs1VDJyo+Zqs9TKLKyD/h7yDpHUhEFsM2CzkICFdoX1av+GBq/J2xRTFfsQO5kBfhZzANf2VcIm84jqDbg== dependencies: ajv "^6.10.2" ajv-keywords "^3.4.1" @@ -15145,6 +15292,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -15549,62 +15703,62 @@ tippy.js@^4.3.5: dependencies: popper.js "^1.14.7" -tiptap-commands@^1.12.3: - version "1.12.3" - resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.3.tgz#604767878073e6344d1daf7a376fd89fc62e4742" - integrity sha512-Dck51lePBwuHmkvkJ6+8V3DbInxAhZwtS2mPvVwz74pDUIcy17tCFw1eHUN50JoXIAci7acuxPKO/weVO1JAyw== +tiptap-commands@^1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.4.tgz#03ef3eda290f0d2ed71a54d73d7619452b05dced" + integrity sha512-szgSZzd/5FHn3Hs02zxySLxNEwNqbyqPOmz5NEAkIJmcyatTkuL+RQDKHA/RNQRUT66SsUCrEXgL1osTqdOSyQ== dependencies: - prosemirror-commands "^1.0.8" - prosemirror-inputrules "^1.0.4" - prosemirror-model "^1.7.4" - prosemirror-schema-list "^1.0.4" - prosemirror-state "^1.2.4" - prosemirror-tables "^0.9.5" - prosemirror-utils "^0.9.6" - tiptap-utils "^1.8.2" + prosemirror-commands "1.1.2" + prosemirror-inputrules "1.1.2" + prosemirror-model "1.8.2" + prosemirror-schema-list "1.1.2" + prosemirror-state "1.3.2" + prosemirror-tables "1.0.0" + prosemirror-utils "0.9.6" + tiptap-utils "^1.8.3" -tiptap-extensions@~1.28.4: - version "1.28.4" - resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.28.4.tgz#0e729d081a80105730101512e7eb5acdce8b9bde" - integrity sha512-UAtxngKifjrMtJFmi3D9RCNC5LJutq4yn1Np0cqJ4dTnvhWR49PqN6gKjlMYyzyutiLLQk+/3GM/E6EfVwmHOA== +tiptap-extensions@~1.28.5: + version "1.28.5" + resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.28.5.tgz#6fba6f7c61abd82729f413f3afa68438b0ba8dd7" + integrity sha512-WvwRvznzgELeSA9JIFse4xNlDEcQ0JMN2PV2sybyPamKM1cvqrYBwF6fqf+EKGmrvwJzmr33CFZpMuzrMeAmWw== dependencies: - lowlight "^1.12.1" - prosemirror-collab "^1.1.2" - prosemirror-history "^1.0.4" - prosemirror-model "^1.7.4" - prosemirror-state "^1.2.4" - prosemirror-tables "^0.9.5" - prosemirror-transform "^1.1.5" - prosemirror-utils "^0.9.6" - prosemirror-view "^1.11.7" - tiptap "^1.26.4" - tiptap-commands "^1.12.3" + lowlight "1.13.0" + prosemirror-collab "1.2.2" + prosemirror-history "1.1.2" + prosemirror-model "1.8.2" + prosemirror-state "1.3.2" + prosemirror-tables "1.0.0" + prosemirror-transform "1.2.2" + prosemirror-utils "0.9.6" + prosemirror-view "1.13.4" + tiptap "^1.26.5" + tiptap-commands "^1.12.4" -tiptap-utils@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.2.tgz#f07a2053c6ac9fbbb4f02e0844b326d0e6c8b7fb" - integrity sha512-pyx+3p4fICGM7JU1mcsnRx5jXvLrCL8Nm/9yjeWEZXpAC85L/btY0eFo2Oz4+dKg39+1EGNHheodujx3ngw4lQ== +tiptap-utils@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.3.tgz#fdfc8f7888f6e9ed0dae081f5f66b9f5429608a9" + integrity sha512-SgqDTCA5ux17KKTpEV2YC54ugBWU2jzpiFlCmVckPjYl5BhmOwuJ1Q5H/8v/XGcnHDqP31Ui4lk31Vts4NmtTA== dependencies: - prosemirror-model "^1.7.4" - prosemirror-state "^1.2.4" - prosemirror-tables "^0.9.5" - prosemirror-utils "^0.9.6" + prosemirror-model "1.8.2" + prosemirror-state "1.3.2" + prosemirror-tables "1.0.0" + prosemirror-utils "0.9.6" -tiptap@^1.26.4, tiptap@~1.26.3: - version "1.26.4" - resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.4.tgz#bfa289841bc45c6401cbd1661a02b81c3d3f14f0" - integrity sha512-UCH0wufjGdKMuCUydL896sFYXEUWC3bE20h/oONABSf0gull+pqBEm7J1yCl7j50eYa9FiLgUBGPqPTzKLluxQ== +tiptap@^1.26.5, tiptap@~1.26.3: + version "1.26.5" + resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.5.tgz#d35f000e0bf93d97532357a29692fa7655e396da" + integrity sha512-PTm9w/UGDQTq6TEjyrNCpNBq9+ZbNU8aZrl+5KLLcmVyMpWCXRd/29b7nKqil8cmi0zUlLrQb9vHteExEgyyrg== dependencies: - prosemirror-commands "^1.0.8" - prosemirror-dropcursor "^1.2.0" - prosemirror-gapcursor "^1.0.4" - prosemirror-inputrules "^1.0.4" - prosemirror-keymap "^1.0.2" - prosemirror-model "^1.7.4" - prosemirror-state "^1.2.4" - prosemirror-view "^1.11.7" - tiptap-commands "^1.12.3" - tiptap-utils "^1.8.2" + prosemirror-commands "1.1.2" + prosemirror-dropcursor "1.3.2" + prosemirror-gapcursor "1.1.2" + prosemirror-inputrules "1.1.2" + prosemirror-keymap "1.1.3" + prosemirror-model "1.8.2" + prosemirror-state "1.3.2" + prosemirror-view "1.13.4" + tiptap-commands "^1.12.4" + tiptap-utils "^1.8.3" title-case@^2.1.0: version "2.1.1" @@ -16257,10 +16411,10 @@ vscode-uri@^1.0.6: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59" integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ== -vue-apollo@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.0.tgz#e252130b18cbd7b0d060fc3dd9616813e4a65acf" - integrity sha512-ByeKajmgItICrOkUl2j/XzqWjv2FOdQYAPsuGyry4yrQBCU641gSoZZn1TjHiR9rAsR2aycGsY9vuV0sN14Mbg== +vue-apollo@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.1.tgz#b7c24b6d6032bf707be7872e6615d59aa6621241" + integrity sha512-NM+kWbPGV5bnRMK7BmMJMxoT1NqPjVAYf+MsjPDyfQNgyVEHSIObRVqLQDIs56PYQSC6YOGa0luo6Ykjj6rrPw== dependencies: chalk "^2.4.2" serialize-javascript "^2.1.0" @@ -16479,10 +16633,10 @@ w3c-keyname@^1.1.8: resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c" integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA== -w3c-keyname@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.1.1.tgz#d234195b0a79913d06a0bc636f17f00050a5de56" - integrity sha512-8kSrsGClLiL4kb5/pTxglejUlEAPk3GXtkBblSMrQDxKz0NkMRTVTPBZm6QCNqPOCPsdNvae5XfV+RJZgeGXEA== +w3c-keyname@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.1.tgz#c4fe1a4b9e303c95e833b3d854b7d81070400db9" + integrity sha512-j5k4xGK6k8TCna/08778KUEL98WvTogiG/TN/YStl8GNeXg5tI3Dvq3+JjwJhP4l7ogs6KWo1VYEdc1Qaioy3Q== walker@^1.0.7, walker@~1.0.5: version "1.0.7" diff --git a/yarn.lock b/yarn.lock index 4493c8492..32999aded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,26 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.7.2": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.4.tgz#37e864532200cb6b50ee9a4045f5f817840166ab" + integrity sha512-+bYbx56j4nYBmpsWtnPUsKW3NdnYxbqyfrP2w9wILBuHzdfIKz9prieZK0DFPyIzkjYVUe4QkusGL07r5pXznQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.7.4" + "@babel/helpers" "^7.7.4" + "@babel/parser" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.0.tgz#c230e79589ae7a729fd4631b9ded4dc220418196" @@ -58,6 +78,16 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" + integrity sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg== + dependencies: + "@babel/types" "^7.7.4" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -65,6 +95,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-annotate-as-pure@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.4.tgz#bb3faf1e74b74bd547e867e48f551fa6b098b6ce" + integrity sha512-2BQmQgECKzYKFPpiycoF9tlb5HA4lrVyAmLLVK177EcQAqjVLciUb2/R+n1boQ9y5ENV3uz2ZqiNw7QMBBw1Og== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f" @@ -73,6 +110,14 @@ "@babel/helper-explode-assignable-expression" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.7.4.tgz#5f73f2b28580e224b5b9bd03146a4015d6217f5f" + integrity sha512-Biq/d/WtvfftWZ9Uf39hbPBYDUo986m5Bb4zhkeYDGUllF43D+nUe5M6Vuo6/8JDK/0YX/uBdeoQpyaNhNugZQ== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-builder-react-jsx@^7.3.0": version "7.3.0" resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz#a1ac95a5d2b3e88ae5e54846bf462eeb81b318a4" @@ -90,6 +135,15 @@ "@babel/traverse" "^7.4.4" "@babel/types" "^7.4.4" +"@babel/helper-call-delegate@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.7.4.tgz#621b83e596722b50c0066f9dc37d3232e461b801" + integrity sha512-8JH9/B7J7tCYJ2PpWVpw9JhPuEVHztagNVuQAFBVFYluRMlpG7F1CgKEgGeL6KFqcsIa92ZYVj6DSc0XwmN1ZA== + dependencies: + "@babel/helper-hoist-variables" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-create-class-features-plugin@^7.3.0": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz#401f302c8ddbc0edd36f7c6b2887d8fa1122e5a4" @@ -102,6 +156,14 @@ "@babel/helper-replace-supers" "^7.5.5" "@babel/helper-split-export-declaration" "^7.4.4" +"@babel/helper-create-regexp-features-plugin@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.4.tgz#6d5762359fd34f4da1500e4cff9955b5299aaf59" + integrity sha512-Mt+jBKaxL0zfOIWrfQpnfYCN7/rS6GKx6CCCfuoqVVd+17R8zNDlzVYmIi9qyb2wOk002NsmSTDymkIygDUH7A== + dependencies: + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.6.0" + "@babel/helper-define-map@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz#3dec32c2046f37e09b28c93eb0b103fd2a25d369" @@ -111,6 +173,15 @@ "@babel/types" "^7.5.5" lodash "^4.17.13" +"@babel/helper-define-map@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.7.4.tgz#2841bf92eb8bd9c906851546fe6b9d45e162f176" + integrity sha512-v5LorqOa0nVQUvAUTUF3KPastvUt/HzByXNamKQ6RdJRTV7j8rLL+WB5C/MzzWAwOomxDhYFb1wLLxHqox86lg== + dependencies: + "@babel/helper-function-name" "^7.7.4" + "@babel/types" "^7.7.4" + lodash "^4.17.13" + "@babel/helper-explode-assignable-expression@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6" @@ -119,6 +190,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-explode-assignable-expression@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.7.4.tgz#fa700878e008d85dc51ba43e9fb835cddfe05c84" + integrity sha512-2/SicuFrNSXsZNBxe5UGdLr+HZg+raWBLE9vC98bdYOKX/U6PY0mdGlYUJdtTDPSU0Lw0PNbKKDpwYHJLn2jLg== + dependencies: + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-function-name@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" @@ -128,6 +207,15 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-function-name@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" + integrity sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ== + dependencies: + "@babel/helper-get-function-arity" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-get-function-arity@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" @@ -135,6 +223,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-get-function-arity@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" + integrity sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-hoist-variables@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" @@ -142,6 +237,13 @@ dependencies: "@babel/types" "^7.4.4" +"@babel/helper-hoist-variables@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.7.4.tgz#612384e3d823fdfaaf9fce31550fe5d4db0f3d12" + integrity sha512-wQC4xyvc1Jo/FnLirL6CEgPgPCa8M74tOdjWpRhQYapz5JC7u3NYU1zCVoVAGCE3EaIP9T1A3iW0WLJ+reZlpQ== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-member-expression-to-functions@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590" @@ -149,6 +251,13 @@ dependencies: "@babel/types" "^7.5.5" +"@babel/helper-member-expression-to-functions@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz#356438e2569df7321a8326644d4b790d2122cb74" + integrity sha512-9KcA1X2E3OjXl/ykfMMInBK+uVdfIVakVe7W7Lg3wfXUNyS3Q1HWLFRwZIjhqiCGbslummPDnmb7vIekS0C1vw== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-module-imports@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" @@ -156,6 +265,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-module-imports@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz#e5a92529f8888bf319a6376abfbd1cebc491ad91" + integrity sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-module-transforms@^7.1.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz#ab2f8e8d231409f8370c883d20c335190284b963" @@ -180,6 +296,18 @@ "@babel/types" "^7.5.5" lodash "^4.17.13" +"@babel/helper-module-transforms@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.7.4.tgz#8d7cdb1e1f8ea3d8c38b067345924ac4f8e0879a" + integrity sha512-ehGBu4mXrhs0FxAqN8tWkzF8GSIGAiEumu4ONZ/hD9M88uHcD+Yu2ttKfOCgwzoesJOJrtQh7trI5YPbRtMmnA== + dependencies: + "@babel/helper-module-imports" "^7.7.4" + "@babel/helper-simple-access" "^7.7.4" + "@babel/helper-split-export-declaration" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/types" "^7.7.4" + lodash "^4.17.13" + "@babel/helper-optimise-call-expression@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5" @@ -187,6 +315,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-optimise-call-expression@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz#034af31370d2995242aa4df402c3b7794b2dcdf2" + integrity sha512-VB7gWZ2fDkSuqW6b1AKXkJWO5NyNI3bFL/kK79/30moK57blr6NbH8xcl2XcKCwOmJosftWunZqfO84IGq3ZZg== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-plugin-utils@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" @@ -217,6 +352,17 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-remap-async-to-generator@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.7.4.tgz#c68c2407350d9af0e061ed6726afb4fff16d0234" + integrity sha512-Sk4xmtVdM9sA/jCI80f+KS+Md+ZHIpjuqmYPk1M7F/upHou5e4ReYmExAiu6PVe65BhJPZA2CY9x9k4BqE5klw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.7.4" + "@babel/helper-wrap-function" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-replace-supers@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2" @@ -227,6 +373,16 @@ "@babel/traverse" "^7.5.5" "@babel/types" "^7.5.5" +"@babel/helper-replace-supers@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz#3c881a6a6a7571275a72d82e6107126ec9e2cdd2" + integrity sha512-pP0tfgg9hsZWo5ZboYGuBn/bbYT/hdLPVSS4NMmiRJdwWhP0IznPwN9AE1JwyGsjSPLC364I0Qh5p+EPkGPNpg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.7.4" + "@babel/helper-optimise-call-expression" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-simple-access@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" @@ -235,6 +391,14 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-simple-access@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.7.4.tgz#a169a0adb1b5f418cfc19f22586b2ebf58a9a294" + integrity sha512-zK7THeEXfan7UlWsG2A6CI/L9jVnI5+xxKZOdej39Y0YtDYKx9raHk5F2EtK9K8DHRTihYwg20ADt9S36GR78A== + dependencies: + "@babel/template" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helper-split-export-declaration@^7.0.0", "@babel/helper-split-export-declaration@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz#571bfd52701f492920d63b7f735030e9a3e10b55" @@ -249,6 +413,13 @@ dependencies: "@babel/types" "^7.4.4" +"@babel/helper-split-export-declaration@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz#57292af60443c4a3622cf74040ddc28e68336fd8" + integrity sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug== + dependencies: + "@babel/types" "^7.7.4" + "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -259,6 +430,16 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" +"@babel/helper-wrap-function@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz#37ab7fed5150e22d9d7266e830072c0cdd8baace" + integrity sha512-VsfzZt6wmsocOaVU0OokwrIytHND55yvyT4BPB9AIIgwr8+x7617hetdJTsuGwygN5RC6mxA9EJztTjuwm2ofg== + dependencies: + "@babel/helper-function-name" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/helpers@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" @@ -268,6 +449,15 @@ "@babel/traverse" "^7.5.5" "@babel/types" "^7.5.5" +"@babel/helpers@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.4.tgz#62c215b9e6c712dadc15a9a0dcab76c92a940302" + integrity sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg== + dependencies: + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -287,6 +477,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== +"@babel/parser@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.4.tgz#75ab2d7110c2cf2fa949959afb05fa346d2231bb" + integrity sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -296,6 +491,15 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" +"@babel/plugin-proposal-async-generator-functions@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.7.4.tgz#0351c5ac0a9e927845fffd5b82af476947b7ce6d" + integrity sha512-1ypyZvGRXriY/QP668+s8sFr2mqinhkRDMPSQLNghCQE+GAkFtp+wkHVvg2+Hdki8gwP+NFzJBJ/N1BfzCCDEw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.7.4" + "@babel/plugin-syntax-async-generators" "^7.7.4" + "@babel/plugin-proposal-class-properties@7.3.0": version "7.3.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd" @@ -304,6 +508,14 @@ "@babel/helper-create-class-features-plugin" "^7.3.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-proposal-dynamic-import@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.4.tgz#dde64a7f127691758cbfed6cf70de0fa5879d52d" + integrity sha512-StH+nGAdO6qDB1l8sZ5UBV8AC3F2VW2I8Vfld73TMKyptMU9DY5YsJAS8U81+vEtxcH3Y/La0wG0btDrhpnhjQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-dynamic-import" "^7.7.4" + "@babel/plugin-proposal-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" @@ -312,6 +524,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" +"@babel/plugin-proposal-json-strings@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.7.4.tgz#7700a6bfda771d8dc81973249eac416c6b4c697d" + integrity sha512-wQvt3akcBTfLU/wYoqm/ws7YOAQKu8EVJEvHip/mzkNtjaclQoCCIqKXFP5/eyfnfbQCDV3OLRIK3mIVyXuZlw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-json-strings" "^7.7.4" + "@babel/plugin-proposal-object-rest-spread@7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" @@ -328,6 +548,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" +"@babel/plugin-proposal-object-rest-spread@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.7.4.tgz#cc57849894a5c774214178c8ab64f6334ec8af71" + integrity sha512-rnpnZR3/iWKmiQyJ3LKJpSwLDcX/nSXhdLk4Aq/tXOApIvyu7qoabrige0ylsAJffaUC51WiBu209Q0U+86OWQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.7.4" + "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" @@ -336,6 +564,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" +"@babel/plugin-proposal-optional-catch-binding@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.7.4.tgz#ec21e8aeb09ec6711bc0a39ca49520abee1de379" + integrity sha512-DyM7U2bnsQerCQ+sejcTNZh8KQEUuC3ufzdnVnSiUv/qoGJp2Z3hanKL18KDhsBT5Wj6a7CMT5mdyCNJsEaA9w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.7.4" + "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" @@ -345,6 +581,14 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" +"@babel/plugin-proposal-unicode-property-regex@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.4.tgz#7c239ccaf09470dbe1d453d50057460e84517ebb" + integrity sha512-cHgqHgYvffluZk85dJ02vloErm3Y6xtH+2noOBOJ2kXOJH3aVCDnj5eR/lVNlTnYu4hndAPJD3rTFjW3qee0PA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-async-generators@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" @@ -352,6 +596,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-async-generators@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.7.4.tgz#331aaf310a10c80c44a66b238b6e49132bd3c889" + integrity sha512-Li4+EjSpBgxcsmeEF8IFcfV/+yJGxHXDirDkEoyFjumuwbmfCVHUt0HuowD/iGM7OhIRyXJH9YXxqiH6N815+g== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-dynamic-import@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.7.4.tgz#29ca3b4415abfe4a5ec381e903862ad1a54c3aec" + integrity sha512-jHQW0vbRGvwQNgyVxwDh4yuXu4bH1f5/EICJLAhl1SblLs2CDhrsmCk+v5XLdE9wxtAFRyxx+P//Iw+a5L/tTg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" @@ -359,6 +617,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-json-strings@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.7.4.tgz#86e63f7d2e22f9e27129ac4e83ea989a382e86cc" + integrity sha512-QpGupahTQW1mHRXddMG5srgpHWqRLwJnJZKXTigB9RPFCCGbDGCgBeM/iC82ICXp414WeYx/tD54w7M2qRqTMg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7" @@ -373,6 +638,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-object-rest-spread@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.7.4.tgz#47cf220d19d6d0d7b154304701f468fc1cc6ff46" + integrity sha512-mObR+r+KZq0XhRVS2BrBKBpr5jqrqzlPvS9C9vuOf5ilSwzloAl7RPWLrgKdWS6IreaVrjHxTjtyqFiOisaCwg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" @@ -380,6 +652,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-optional-catch-binding@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.7.4.tgz#a3e38f59f4b6233867b4a92dcb0ee05b2c334aa6" + integrity sha512-4ZSuzWgFxqHRE31Glu+fEr/MirNZOMYmD/0BhBWyLyOOQz/gTAl7QmWm2hX1QxEIXsr2vkdlwxIzTyiYRC4xcQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-top-level-await@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.4.tgz#bd7d8fa7b9fee793a36e4027fd6dd1aa32f946da" + integrity sha512-wdsOw0MvkL1UIgiQ/IFr3ETcfv1xb8RMM0H9wbiDyLaJFyiDg5oZvDLCXosIXmFeIlweML5iOBXAkqddkYNizg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -387,6 +673,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-arrow-functions@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.7.4.tgz#76309bd578addd8aee3b379d809c802305a98a12" + integrity sha512-zUXy3e8jBNPiffmqkHRNDdZM2r8DWhCB7HhcoyZjiK1TxYEluLHAvQuYnTT+ARqRpabWqy/NHkO6e3MsYB5YfA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-async-to-generator@^7.4.4": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e" @@ -396,6 +689,15 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" +"@babel/plugin-transform-async-to-generator@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.7.4.tgz#694cbeae6d613a34ef0292713fa42fb45c4470ba" + integrity sha512-zpUTZphp5nHokuy8yLlyafxCJ0rSlFoSHypTUWgpdwoDXWQcseaect7cJ8Ppk6nunOM6+5rPMkod4OYKPR5MUg== + dependencies: + "@babel/helper-module-imports" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.7.4" + "@babel/plugin-transform-block-scoped-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" @@ -403,6 +705,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-block-scoped-functions@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.7.4.tgz#d0d9d5c269c78eaea76227ace214b8d01e4d837b" + integrity sha512-kqtQzwtKcpPclHYjLK//3lH8OFsCDuDJBaFhVwf8kqdnF6MN4l618UDlcA7TfRs3FayrHj+svYnSX8MC9zmUyQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-block-scoping@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz#a35f395e5402822f10d2119f6f8e045e3639a2ce" @@ -411,6 +720,14 @@ "@babel/helper-plugin-utils" "^7.0.0" lodash "^4.17.13" +"@babel/plugin-transform-block-scoping@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.7.4.tgz#200aad0dcd6bb80372f94d9e628ea062c58bf224" + integrity sha512-2VBe9u0G+fDt9B5OV5DQH4KBf5DoiNkwFKOz0TCvBWvdAN2rOykCTkrL+jTLxfCAm76l9Qo5OqL7HBOx2dWggg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + lodash "^4.17.13" + "@babel/plugin-transform-classes@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz#d094299d9bd680a14a2a0edae38305ad60fb4de9" @@ -425,6 +742,20 @@ "@babel/helper-split-export-declaration" "^7.4.4" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.7.4.tgz#c92c14be0a1399e15df72667067a8f510c9400ec" + integrity sha512-sK1mjWat7K+buWRuImEzjNf68qrKcrddtpQo3swi9j7dUcG6y6R6+Di039QN2bD1dykeswlagupEmpOatFHHUg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.7.4" + "@babel/helper-define-map" "^7.7.4" + "@babel/helper-function-name" "^7.7.4" + "@babel/helper-optimise-call-expression" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.7.4" + "@babel/helper-split-export-declaration" "^7.7.4" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" @@ -432,6 +763,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-computed-properties@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.7.4.tgz#e856c1628d3238ffe12d668eb42559f79a81910d" + integrity sha512-bSNsOsZnlpLLyQew35rl4Fma3yKWqK3ImWMSC/Nc+6nGjC9s5NFWAer1YQ899/6s9HxO2zQC1WoFNfkOqRkqRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-destructuring@^7.4.4": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" @@ -439,6 +777,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-destructuring@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.7.4.tgz#2b713729e5054a1135097b6a67da1b6fe8789267" + integrity sha512-4jFMXI1Cu2aXbcXXl8Lr6YubCn6Oc7k9lLsu8v61TZh+1jny2BWmdtvY9zSUlLdGUvcy9DMAWyZEOqjsbeg/wA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" @@ -448,6 +793,14 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" +"@babel/plugin-transform-dotall-regex@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.7.4.tgz#f7ccda61118c5b7a2599a72d5e3210884a021e96" + integrity sha512-mk0cH1zyMa/XHeb6LOTXTbG7uIJ8Rrjlzu91pUx/KS3JpcgaTDwMS8kM+ar8SLOvlL2Lofi4CGBAjCo3a2x+lw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-duplicate-keys@^7.2.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853" @@ -455,6 +808,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-duplicate-keys@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.7.4.tgz#3d21731a42e3f598a73835299dd0169c3b90ac91" + integrity sha512-g1y4/G6xGWMD85Tlft5XedGaZBCIVN+/P0bs6eabmcPP9egFleMAo65OOjlhcz1njpwagyY3t0nsQC9oTFegJA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-exponentiation-operator@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" @@ -463,6 +823,14 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-exponentiation-operator@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.7.4.tgz#dd30c0191e3a1ba19bcc7e389bdfddc0729d5db9" + integrity sha512-MCqiLfCKm6KEA1dglf6Uqq1ElDIZwFuzz1WH5mTf8k2uQSxEJMbOIEh7IZv7uichr7PMfi5YVSrr1vz+ipp7AQ== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-for-of@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" @@ -470,6 +838,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-for-of@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.7.4.tgz#248800e3a5e507b1f103d8b4ca998e77c63932bc" + integrity sha512-zZ1fD1B8keYtEcKF+M1TROfeHTKnijcVQm0yO/Yu1f7qoDoxEIc/+GX6Go430Bg84eM/xwPFp0+h4EbZg7epAA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-function-name@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" @@ -478,6 +853,14 @@ "@babel/helper-function-name" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-function-name@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.7.4.tgz#75a6d3303d50db638ff8b5385d12451c865025b1" + integrity sha512-E/x09TvjHNhsULs2IusN+aJNRV5zKwxu1cpirZyRPw+FyyIKEHPXTsadj48bVpc1R5Qq1B5ZkzumuFLytnbT6g== + dependencies: + "@babel/helper-function-name" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-literals@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" @@ -485,6 +868,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-literals@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.7.4.tgz#27fe87d2b5017a2a5a34d1c41a6b9f6a6262643e" + integrity sha512-X2MSV7LfJFm4aZfxd0yLVFrEXAgPqYoDG53Br/tCKiKYfX0MjVjQeWPIhPHHsCqzwQANq+FLN786fF5rgLS+gw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-member-expression-literals@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz#fa10aa5c58a2cb6afcf2c9ffa8cb4d8b3d489a2d" @@ -492,6 +882,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-member-expression-literals@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.7.4.tgz#aee127f2f3339fc34ce5e3055d7ffbf7aa26f19a" + integrity sha512-9VMwMO7i69LHTesL0RdGy93JU6a+qOPuvB4F4d0kR0zyVjJRVJRaoaGjhtki6SzQUu8yen/vxPKN6CWnCUw6bA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-modules-amd@^7.2.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91" @@ -501,6 +898,15 @@ "@babel/helper-plugin-utils" "^7.0.0" babel-plugin-dynamic-import-node "^2.3.0" +"@babel/plugin-transform-modules-amd@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.7.4.tgz#276b3845ca2b228f2995e453adc2e6f54d72fb71" + integrity sha512-/542/5LNA18YDtg1F+QHvvUSlxdvjZoD/aldQwkq+E3WCkbEjNSN9zdrOXaSlfg3IfGi22ijzecklF/A7kVZFQ== + dependencies: + "@babel/helper-module-transforms" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" + "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" @@ -511,6 +917,16 @@ "@babel/helper-simple-access" "^7.1.0" babel-plugin-dynamic-import-node "^2.3.0" +"@babel/plugin-transform-modules-commonjs@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.4.tgz#bee4386e550446343dd52a571eda47851ff857a3" + integrity sha512-k8iVS7Jhc367IcNF53KCwIXtKAH7czev866ThsTgy8CwlXjnKZna2VHwChglzLleYrcHz1eQEIJlGRQxB53nqA== + dependencies: + "@babel/helper-module-transforms" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-simple-access" "^7.7.4" + babel-plugin-dynamic-import-node "^2.3.0" + "@babel/plugin-transform-modules-systemjs@^7.4.4": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249" @@ -520,6 +936,15 @@ "@babel/helper-plugin-utils" "^7.0.0" babel-plugin-dynamic-import-node "^2.3.0" +"@babel/plugin-transform-modules-systemjs@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.7.4.tgz#cd98152339d3e763dfe838b7d4273edaf520bb30" + integrity sha512-y2c96hmcsUi6LrMqvmNDPBBiGCiQu0aYqpHatVVu6kD4mFEXKjyNxd/drc18XXAf9dv7UXjrZwBVmTTGaGP8iw== + dependencies: + "@babel/helper-hoist-variables" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" + "@babel/plugin-transform-modules-umd@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" @@ -528,6 +953,14 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-modules-umd@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.7.4.tgz#1027c355a118de0aae9fee00ad7813c584d9061f" + integrity sha512-u2B8TIi0qZI4j8q4C51ktfO7E3cQ0qnaXFI1/OXITordD40tt17g/sXqgNNCcMTcBFKrUPcGDx+TBJuZxLx7tw== + dependencies: + "@babel/helper-module-transforms" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" @@ -535,6 +968,13 @@ dependencies: regexp-tree "^0.1.6" +"@babel/plugin-transform-named-capturing-groups-regex@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.7.4.tgz#fb3bcc4ee4198e7385805007373d6b6f42c98220" + integrity sha512-jBUkiqLKvUWpv9GLSuHUFYdmHg0ujC1JEYoZUfeOOfNydZXp1sXObgyPatpcwjWgsdBGsagWW0cdJpX/DO2jMw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.7.4" + "@babel/plugin-transform-new-target@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" @@ -542,6 +982,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-new-target@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.7.4.tgz#4a0753d2d60639437be07b592a9e58ee00720167" + integrity sha512-CnPRiNtOG1vRodnsyGX37bHQleHE14B9dnnlgSeEs3ek3fHN1A1SScglTCg1sfbe7sRQ2BUcpgpTpWSfMKz3gg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-object-super@^7.2.0": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz#c70021df834073c65eb613b8679cc4a381d1a9f9" @@ -550,6 +997,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-replace-supers" "^7.5.5" +"@babel/plugin-transform-object-super@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.7.4.tgz#48488937a2d586c0148451bf51af9d7dda567262" + integrity sha512-ho+dAEhC2aRnff2JCA0SAK7V2R62zJd/7dmtoe7MHcso4C2mS+vZjn1Pb1pCVZvJs1mgsvv5+7sT+m3Bysb6eg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.7.4" + "@babel/plugin-transform-parameters@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" @@ -559,6 +1014,15 @@ "@babel/helper-get-function-arity" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-parameters@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.7.4.tgz#da4555c97f39b51ac089d31c7380f03bca4075ce" + integrity sha512-VJwhVePWPa0DqE9vcfptaJSzNDKrWU/4FbYCjZERtmqEs05g3UMXnYMZoXja7JAJ7Y7sPZipwm/pGApZt7wHlw== + dependencies: + "@babel/helper-call-delegate" "^7.7.4" + "@babel/helper-get-function-arity" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-property-literals@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905" @@ -566,6 +1030,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-property-literals@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.7.4.tgz#2388d6505ef89b266103f450f9167e6bd73f98c2" + integrity sha512-MatJhlC4iHsIskWYyawl53KuHrt+kALSADLQQ/HkhTjX954fkxIEh4q5slL4oRAnsm/eDoZ4q0CIZpcqBuxhJQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-react-display-name@^7.0.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz#ebfaed87834ce8dc4279609a4f0c324c156e3eb0" @@ -605,6 +1076,13 @@ dependencies: regenerator-transform "^0.14.0" +"@babel/plugin-transform-regenerator@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.4.tgz#d18eac0312a70152d7d914cbed2dc3999601cfc0" + integrity sha512-e7MWl5UJvmPEwFJTwkBlPmqixCtr9yAASBqff4ggXTNicZiwbF8Eefzm6NVgfiBp7JdAGItecnctKTgH44q2Jw== + dependencies: + regenerator-transform "^0.14.0" + "@babel/plugin-transform-reserved-words@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634" @@ -612,6 +1090,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-reserved-words@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.7.4.tgz#6a7cf123ad175bb5c69aec8f6f0770387ed3f1eb" + integrity sha512-OrPiUB5s5XvkCO1lS7D8ZtHcswIC57j62acAnJZKqGGnHP+TIc/ljQSrgdX/QyOTdEK5COAhuc820Hi1q2UgLQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-runtime@7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.2.0.tgz#566bc43f7d0aedc880eaddbd29168d0f248966ea" @@ -629,6 +1114,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-shorthand-properties@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.7.4.tgz#74a0a9b2f6d67a684c6fbfd5f0458eb7ba99891e" + integrity sha512-q+suddWRfIcnyG5YiDP58sT65AJDZSUhXQDZE3r04AuqD6d/XLaQPPXSBzP2zGerkgBivqtQm9XKGLuHqBID6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-spread@^7.2.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" @@ -636,6 +1128,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-spread@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.7.4.tgz#aa673b356fe6b7e70d69b6e33a17fef641008578" + integrity sha512-8OSs0FLe5/80cndziPlg4R0K6HcWSM0zyNhHhLsmw/Nc5MaA49cAsnoJ/t/YZf8qkG7fD+UjTRaApVDB526d7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-sticky-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" @@ -644,6 +1143,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" +"@babel/plugin-transform-sticky-regex@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.7.4.tgz#ffb68c05090c30732076b1285dc1401b404a123c" + integrity sha512-Ls2NASyL6qtVe1H1hXts9yuEeONV2TJZmplLONkMPUG158CtmnrzW5Q5teibM5UVOFjG0D3IC5mzXR6pPpUY7A== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + "@babel/plugin-transform-template-literals@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" @@ -652,6 +1159,14 @@ "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-template-literals@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.7.4.tgz#1eb6411736dd3fe87dbd20cc6668e5121c17d604" + integrity sha512-sA+KxLwF3QwGj5abMHkHgshp9+rRz+oY9uoRil4CyLtgEuE/88dpkeWgNk5qKVsJE9iSfly3nvHapdRiIS2wnQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typeof-symbol@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" @@ -659,6 +1174,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typeof-symbol@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.7.4.tgz#3174626214f2d6de322882e498a38e8371b2140e" + integrity sha512-KQPUQ/7mqe2m0B8VecdyaW5XcQYaePyl9R7IsKd+irzj6jvbhoGnRE+M0aNkyAzI07VfUQ9266L5xMARitV3wg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-unicode-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" @@ -668,6 +1190,14 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" +"@babel/plugin-transform-unicode-regex@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.7.4.tgz#a3c0f65b117c4c81c5b6484f2a5e7b95346b83ae" + integrity sha512-N77UUIV+WCvE+5yHw+oks3m18/umd7y392Zv7mYTpFqHtkpcc+QUz+gLJNTWVlWROIWeLqY0f3OjZxV5TcXnRw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/preset-env@7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58" @@ -722,6 +1252,63 @@ js-levenshtein "^1.1.3" semver "^5.5.0" +"@babel/preset-env@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.7.4.tgz#ccaf309ae8d1ee2409c85a4e2b5e280ceee830f8" + integrity sha512-Dg+ciGJjwvC1NIe/DGblMbcGq1HOtKbw8RLl4nIjlfcILKEOkWT/vRqPpumswABEBVudii6dnVwrBtzD7ibm4g== + dependencies: + "@babel/helper-module-imports" "^7.7.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.7.4" + "@babel/plugin-proposal-dynamic-import" "^7.7.4" + "@babel/plugin-proposal-json-strings" "^7.7.4" + "@babel/plugin-proposal-object-rest-spread" "^7.7.4" + "@babel/plugin-proposal-optional-catch-binding" "^7.7.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.7.4" + "@babel/plugin-syntax-async-generators" "^7.7.4" + "@babel/plugin-syntax-dynamic-import" "^7.7.4" + "@babel/plugin-syntax-json-strings" "^7.7.4" + "@babel/plugin-syntax-object-rest-spread" "^7.7.4" + "@babel/plugin-syntax-optional-catch-binding" "^7.7.4" + "@babel/plugin-syntax-top-level-await" "^7.7.4" + "@babel/plugin-transform-arrow-functions" "^7.7.4" + "@babel/plugin-transform-async-to-generator" "^7.7.4" + "@babel/plugin-transform-block-scoped-functions" "^7.7.4" + "@babel/plugin-transform-block-scoping" "^7.7.4" + "@babel/plugin-transform-classes" "^7.7.4" + "@babel/plugin-transform-computed-properties" "^7.7.4" + "@babel/plugin-transform-destructuring" "^7.7.4" + "@babel/plugin-transform-dotall-regex" "^7.7.4" + "@babel/plugin-transform-duplicate-keys" "^7.7.4" + "@babel/plugin-transform-exponentiation-operator" "^7.7.4" + "@babel/plugin-transform-for-of" "^7.7.4" + "@babel/plugin-transform-function-name" "^7.7.4" + "@babel/plugin-transform-literals" "^7.7.4" + "@babel/plugin-transform-member-expression-literals" "^7.7.4" + "@babel/plugin-transform-modules-amd" "^7.7.4" + "@babel/plugin-transform-modules-commonjs" "^7.7.4" + "@babel/plugin-transform-modules-systemjs" "^7.7.4" + "@babel/plugin-transform-modules-umd" "^7.7.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.7.4" + "@babel/plugin-transform-new-target" "^7.7.4" + "@babel/plugin-transform-object-super" "^7.7.4" + "@babel/plugin-transform-parameters" "^7.7.4" + "@babel/plugin-transform-property-literals" "^7.7.4" + "@babel/plugin-transform-regenerator" "^7.7.4" + "@babel/plugin-transform-reserved-words" "^7.7.4" + "@babel/plugin-transform-shorthand-properties" "^7.7.4" + "@babel/plugin-transform-spread" "^7.7.4" + "@babel/plugin-transform-sticky-regex" "^7.7.4" + "@babel/plugin-transform-template-literals" "^7.7.4" + "@babel/plugin-transform-typeof-symbol" "^7.7.4" + "@babel/plugin-transform-unicode-regex" "^7.7.4" + "@babel/types" "^7.7.4" + browserslist "^4.6.0" + core-js-compat "^3.1.1" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.5.0" + "@babel/preset-react@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" @@ -733,6 +1320,25 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/register@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.7.4.tgz#45a4956471a9df3b012b747f5781cc084ee8f128" + integrity sha512-/fmONZqL6ZMl9KJUYajetCrID6m0xmL4odX7v+Xvoxcv0DdbP/oO0TWIeLUCHqczQ6L6njDMqmqHFy2cp3FFsA== + dependencies: + find-cache-dir "^2.0.0" + lodash "^4.17.13" + make-dir "^2.1.0" + pirates "^4.0.0" + source-map-support "^0.5.16" + +"@babel/runtime-corejs2@^7.2.0": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.7.4.tgz#b9c2b1b5882762005785bc47740195a0ac780888" + integrity sha512-hKNcmHQbBSJFnZ82ewYtWDZ3fXkP/l1XcfRtm7c8gHPM/DMecJtFFBEp7KMLZTuHwwb7RfemHdsEnd7L916Z6A== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" + "@babel/runtime@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" @@ -765,6 +1371,15 @@ "@babel/parser" "^7.4.4" "@babel/types" "^7.4.4" +"@babel/template@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" + integrity sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.7.4" + "@babel/types" "^7.7.4" + "@babel/traverse@^7.1.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.0.tgz#14006967dd1d2b3494cdd650c686db9daf0ddada" @@ -795,6 +1410,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" + integrity sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.7.4" + "@babel/helper-function-name" "^7.7.4" + "@babel/helper-split-export-declaration" "^7.7.4" + "@babel/parser" "^7.7.4" + "@babel/types" "^7.7.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.0.tgz#670724f77d24cce6cc7d8cf64599d511d164894c" @@ -813,6 +1443,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" + integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cypress/browserify-preprocessor@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@cypress/browserify-preprocessor/-/browserify-preprocessor-2.1.1.tgz#93e7c1228fee9dea9e80a1ccf0dd8cca1ffcbad8" @@ -891,11 +1530,89 @@ dependencies: "@hapi/hoek" "8.x.x" +"@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/yargs-parser@*": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" + integrity sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg== + +"@types/yargs@^13.0.0": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.3.tgz#76482af3981d4412d65371a318f992d33464a380" + integrity sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ== + dependencies: + "@types/yargs-parser" "*" + JSONStream@^1.0.3: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -966,18 +1683,31 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-regex@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= -ansi-styles@^3.2.1: +ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.0.tgz#5681f0dcf7ae5880a7841d8831c4724ed9cc0172" + integrity sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1093,6 +1823,15 @@ assertion-error-formatter@^2.0.1: pad-right "^0.2.2" repeat-string "^1.6.1" +assertion-error-formatter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz#be9c8825dee6a8a6c72183d915912d9b57d5d265" + integrity sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ== + dependencies: + diff "^4.0.1" + pad-right "^0.2.2" + repeat-string "^1.6.1" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -1211,7 +1950,7 @@ bcryptjs@^2.4.3: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= -becke-ch--regex--s0-0-v1--base--pl--lib@^1.2.0: +becke-ch--regex--s0-0-v1--base--pl--lib@^1.2.0, becke-ch--regex--s0-0-v1--base--pl--lib@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/becke-ch--regex--s0-0-v1--base--pl--lib/-/becke-ch--regex--s0-0-v1--base--pl--lib-1.4.0.tgz#429ceebbfa5f7e936e78d73fbdc7da7162b20e20" integrity sha1-Qpzuu/pffpNueNc/vcfacWKyDiA= @@ -1461,6 +2200,11 @@ cachedir@1.3.0: dependencies: os-homedir "^1.0.1" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + caniuse-lite@^1.0.30000984: version "1.0.30000989" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" @@ -1503,6 +2247,14 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -1572,6 +2324,16 @@ cli-spinners@^0.1.2: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= +cli-table3@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" + integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== + dependencies: + object-assign "^4.1.0" + string-width "^2.1.1" + optionalDependencies: + colors "^1.1.2" + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -1631,11 +2393,23 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -1683,6 +2457,11 @@ common-tags@1.8.0: resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -1727,6 +2506,13 @@ convert-source-map@^1.1.0, convert-source-map@^1.3.0: dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + convert-source-map@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" @@ -1750,6 +2536,11 @@ core-js@^2.4.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== +core-js@^2.6.5: + version "2.6.10" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" + integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== + core-js@^3.2.1: version "3.3.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.3.3.tgz#b7048d3c6c1a52b5fe55a729c1d4ccdffe0891bb" @@ -1867,11 +2658,24 @@ cucumber-expressions@^6.0.1: dependencies: becke-ch--regex--s0-0-v1--base--pl--lib "^1.2.0" +cucumber-expressions@^8.1.0: + version "8.2.1" + resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-8.2.1.tgz#e250063993350df106a8664c90a414814f555e2d" + integrity sha512-6n5JKbAzXfIiwyu2UyUcOmO83QmuSme25+Dw2taK6VNOybOfRkh4yNMA9VtuAJHOmsX3/8l0OVjTbE8lHnjOHA== + dependencies: + becke-ch--regex--s0-0-v1--base--pl--lib "^1.4.0" + xregexp "^4.2.4" + cucumber-tag-expressions@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/cucumber-tag-expressions/-/cucumber-tag-expressions-1.1.1.tgz#7f5c7b70009bc2b666591bfe64854578bedee85a" integrity sha1-f1x7cACbwrZmWRv+ZIVFeL7e6Fo= +cucumber-tag-expressions@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/cucumber-tag-expressions/-/cucumber-tag-expressions-2.0.2.tgz#aac27aae3690818ec15235bd056282dad8a2d2b8" + integrity sha512-DohmT4X641KX/sb96bdb7J2kXNcQBPrYmf3Oc5kiHCLfzFMWx/o2kB4JvjvQPZnYuA9lRt6pqtArM5gvUn4uzw== + cucumber@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-4.2.1.tgz#64cfff6150bbe6b5e94b173470057353d6206719" @@ -1906,10 +2710,43 @@ cucumber@^4.2.1: util-arity "^1.0.2" verror "^1.9.0" -cypress-cucumber-preprocessor@^1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.16.2.tgz#79e0ce7e7afa781f752711f7284a3bf48aa99ab8" - integrity sha512-jDJuQnnzrOrO+4PRt+VKFkHxHO7DplJACXOMUHLLWcL7vjlRUkIG4+QWnOkn/Py3yOhv9Rmuug8Iil5+FV8wEw== +cucumber@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-6.0.5.tgz#cdc752ad18b551bcf7bc92774c925302f4408714" + integrity sha512-x+W9Fwk6TvcapQsYMxwFU5AsQJDOIJVGrPKmH15OC7jzb9/Dk7Hb0ZAyw4WcpaDcUDRc8bi2k2yJejDp5eTRlg== + dependencies: + assertion-error-formatter "^3.0.0" + bluebird "^3.4.1" + cli-table3 "^0.5.1" + colors "^1.1.2" + commander "^3.0.1" + cucumber-expressions "^8.1.0" + cucumber-tag-expressions "^2.0.2" + duration "^0.2.1" + escape-string-regexp "^2.0.0" + figures "^3.0.0" + gherkin "5.0.0" + glob "^7.1.3" + indent-string "^4.0.0" + is-generator "^1.0.2" + is-stream "^2.0.0" + knuth-shuffle-seeded "^1.0.6" + lodash "^4.17.14" + mz "^2.4.0" + progress "^2.0.0" + resolve "^1.3.3" + serialize-error "^4.1.0" + stack-chain "^2.0.0" + stacktrace-js "^2.0.0" + string-argv "^0.3.0" + title-case "^2.1.1" + util-arity "^1.0.2" + verror "^1.9.0" + +cypress-cucumber-preprocessor@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.17.0.tgz#c0f60f1e4b408300bd46acbf0be29d791b299771" + integrity sha512-nj3qIgb3DDRzeefWZoFr78XuLKkPmMzEvw3apgADWsR8j16C6stoJQYtN0+HYrL9B30nPeJzW7bbgHMHbps0Zg== dependencies: "@cypress/browserify-preprocessor" "^2.1.1" chai "^4.1.2" @@ -1929,15 +2766,17 @@ cypress-file-upload@^3.5.0: resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.5.0.tgz#7ff6294a3be68adcac72bc1fa25c75c57e0701fc" integrity sha512-MgrsP4C2pUMvHqixH33OtXHNKkwQZV6ny0fu+APDTEP9O/uuKFh3sYcnPNU6XGfuC85/RmqVNw21ZQyyT+S5IQ== -cypress-plugin-retries@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.4.0.tgz#30477294a22e368c874d50dc282e657906080955" - integrity sha512-Pudna9+dn0wp3flUVWt1ttn6hKTnD1MIBUSznYkw+uRv3JPNJhxHIv9cfxrZmig49/R1fIyGBVNORchtnFedEw== +cypress-plugin-retries@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.5.0.tgz#717cdfd7f2a8f07f218eb23e93ff6f7aa4c95029" + integrity sha512-nSJx4RkbeQFGjsymPWabn7i2GmxULUu9SqtiJqoVFa1XfVtQC1Uoogxj/zZzhrWYwYJ2183xslb9HqQV6bafFQ== + dependencies: + chalk "^3.0.0" -cypress@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.6.1.tgz#4420957923879f60b7a5146ccbf81841a149b653" - integrity sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A== +cypress@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.7.0.tgz#e2cd71b87b6ce0d4c72c6ea25da1005d75c1f231" + integrity sha512-o+vfRxqAba8TduelzfZQ4WHmj2yNEjaoO2EuZ8dZ9pJpuW+WGtBGheKIp6zkoQsp8ZgFe8OoHh1i2mY8BDnMAw== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/xvfb" "1.2.4" @@ -2120,11 +2959,21 @@ detective@^5.0.2: defined "^1.0.0" minimist "^1.1.1" +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + diff@^3.0.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -2156,7 +3005,7 @@ duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: dependencies: readable-stream "^2.0.2" -duration@^0.2.0: +duration@^0.2.0, duration@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/duration/-/duration-0.2.2.tgz#ddf149bc3bc6901150fe9017111d016b3357f529" integrity sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg== @@ -2273,6 +3122,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -2334,6 +3188,18 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -2431,6 +3297,13 @@ figures@^1.7.0: escape-string-regexp "^1.0.5" object-assign "^4.1.0" +figures@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" + integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== + dependencies: + escape-string-regexp "^1.0.5" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -2441,6 +3314,22 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +find-cache-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -2558,6 +3447,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gherkin@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/gherkin/-/gherkin-5.0.0.tgz#96def41198ec3908258b511af74f655a2764d2a1" + integrity sha1-lt70EZjsOQgli1Ea909lWidk0qE= + gherkin@^5.0.0, gherkin@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/gherkin/-/gherkin-5.1.0.tgz#684bbb03add24eaf7bdf544f58033eb28fb3c6d5" @@ -2600,6 +3494,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +graceful-fs@^4.1.15: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + graphql-request@^1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-1.8.2.tgz#398d10ae15c585676741bde3fc01d5ca948f8fbe" @@ -2643,6 +3542,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -2779,6 +3683,11 @@ indent-string@^3.0.0, indent-string@^3.1.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3018,6 +3927,11 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + is-symbol@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" @@ -3067,6 +3981,50 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -3272,6 +4230,14 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + lodash.clonedeep@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -3292,7 +4258,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.15, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.4: +lodash@4.17.15, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -3331,6 +4297,14 @@ lower-case@^1.1.1: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -3585,6 +4559,11 @@ node-fetch@^2.2.0, node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + node-pre-gyp@^0.10.0: version "0.10.3" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" @@ -3806,11 +4785,30 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-limit@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" + integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + pad-right@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" @@ -3870,6 +4868,11 @@ path-dirname@^1.0.0: resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -3948,11 +4951,40 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pirates@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + private@^0.1.6: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -4050,6 +5082,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.8.4: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" @@ -4095,6 +5132,13 @@ regenerate-unicode-properties@^8.0.2: dependencies: regenerate "^1.4.0" +regenerate-unicode-properties@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" + integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA== + dependencies: + regenerate "^1.4.0" + regenerate@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" @@ -4147,6 +5191,18 @@ regexpu-core@^4.5.4: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.1.0" +regexpu-core@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6" + integrity sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.1.0" + regjsgen "^0.5.0" + regjsparser "^0.6.0" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.1.0" + regjsgen@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" @@ -4303,6 +5359,11 @@ seed-random@~2.2.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== +semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -4313,6 +5374,13 @@ serialize-error@^2.1.0: resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= +serialize-error@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-4.1.0.tgz#63e1e33ede20bcd89d9f0528ea4c15fbf0f2b78a" + integrity sha512-5j9GgyGsP9vV9Uj1S0lDCvlsd+gc2LEPVK7HHHte7IyPwOD4lVQFeaX143gx3U5AnoCi+wbcb3mvaxVysjpxEw== + dependencies: + type-fest "^0.3.0" + set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4376,6 +5444,11 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" @@ -4429,6 +5502,14 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-support@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -4444,7 +5525,7 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -4514,6 +5595,11 @@ stack-generator@^2.0.1: dependencies: stackframe "^1.0.4" +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + stackframe@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" @@ -4589,6 +5675,11 @@ string-argv@0.0.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736" integrity sha1-2sMECGkMIfPDYwo/86BYd73L1zY= +string-argv@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -4598,7 +5689,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2": +"string-width@^1.0.2 || 2", string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -4677,6 +5768,13 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" @@ -4840,6 +5938,11 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" + integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5058,6 +6161,13 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +xregexp@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.2.4.tgz#02a4aea056d65a42632c02f0233eab8e4d7e57ed" + integrity sha512-sO0bYdYeJAJBcJA8g7MJJX7UrOZIfJPd8U2SC7B2Dd/J24U0aQNoGp33shCaBSWeb0rD5rh6VBUIXOkGal1TZA== + dependencies: + "@babel/runtime-corejs2" "^7.2.0" + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"