diff --git a/.codecov.yml b/.codecov.yml index 2767ed675..82b50aba3 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -95,7 +95,7 @@ coverage: # - master #flags: # - integration - paths: + paths: - backend/ # only include coverage in "backend/" folder webapp: # declare a new status context "frontend" against: parent @@ -127,7 +127,7 @@ coverage: # - integration # paths: # - folder - + #changes: # default: # against: parent @@ -150,20 +150,8 @@ coverage: #ignore: # files and folders for processing # - tests/* - + #fixes: # - "old_path::new_path" -comment: - # layout options are quite limited in v4.x - there have been way more options in v1.0 - layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags - behavior: new # default = posts once then update, posts new if delete - # once = post once then updates - # new = delete old, post new - # spammy = post new - require_changes: false # if true: only post the comment if coverage changes - require_base: no # [yes :: must have a base report to post] - require_head: no # [yes :: must have a head report to post] - branches: null # branch names that can post comment - flags: null - paths: null \ No newline at end of file +comment: off diff --git a/.travis.yml b/.travis.yml index 593b83e5f..3c1208496 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ addons: before_install: - yarn global add wait-on # Install Codecov - - yarn global add codecov - yarn install - cp cypress.env.template.json cypress.env.json @@ -40,7 +39,7 @@ script: # Fullstack - yarn run cypress:run # Coverage - - codecov + - yarn run codecov after_success: - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh diff --git a/backend/package.json b/backend/package.json index 4339f32ed..d9672ec6b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,7 +34,6 @@ "!**/src/**/?(*.)+(spec|test).js?(x)" ], "coverageReporters": [ - "text", "lcov" ], "testMatch": [ @@ -48,7 +47,8 @@ "apollo-client": "~2.6.3", "apollo-link-context": "~1.0.18", "apollo-link-http": "~1.5.15", - "apollo-server": "~2.6.6", + "apollo-server": "~2.6.9", + "apollo-server-express": "^2.6.9", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", @@ -56,7 +56,7 @@ "date-fns": "2.0.0-beta.1", "debug": "~4.1.1", "dotenv": "~8.0.0", - "express": "~4.17.1", + "express": "^4.17.1", "faker": "Marak/faker.js#master", "graphql": "~14.4.2", "graphql-custom-directives": "~0.2.14", @@ -64,33 +64,32 @@ "graphql-middleware": "~3.0.2", "graphql-shield": "~6.0.3", "graphql-tag": "~2.10.1", - "graphql-yoga": "~1.18.0", "helmet": "~3.18.0", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", - "lodash": "~4.17.11", + "lodash": "~4.17.14", "merge-graphql-schemas": "^1.5.8", "neo4j-driver": "~1.7.4", "neo4j-graphql-js": "^2.6.3", "neode": "^0.2.16", "node-fetch": "~2.6.0", - "nodemailer": "^6.2.1", + "nodemailer": "^6.3.0", "npm-run-all": "~4.1.5", "request": "~2.88.0", "sanitize-html": "~1.20.1", "slug": "~1.1.0", "trunc-html": "~1.1.2", "uuid": "~3.3.2", - "wait-on": "~3.2.0" + "wait-on": "~3.3.0" }, "devDependencies": { "@babel/cli": "~7.5.0", - "@babel/core": "~7.5.0", + "@babel/core": "~7.5.4", "@babel/node": "~7.5.0", "@babel/plugin-proposal-throw-expressions": "^7.2.0", - "@babel/preset-env": "~7.5.2", + "@babel/preset-env": "~7.5.4", "@babel/register": "~7.4.4", - "apollo-server-testing": "~2.6.7", + "apollo-server-testing": "~2.7.0", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.2", "babel-jest": "~24.8.0", @@ -100,7 +99,7 @@ "eslint-config-prettier": "~6.0.0", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.18.0", - "eslint-plugin-jest": "~22.7.2", + "eslint-plugin-jest": "~22.9.0", "eslint-plugin-node": "~9.1.0", "eslint-plugin-prettier": "~3.1.0", "eslint-plugin-promise": "~4.2.1", diff --git a/backend/src/bootstrap/neode.js b/backend/src/bootstrap/neode.js index 419cb1032..65a2074be 100644 --- a/backend/src/bootstrap/neode.js +++ b/backend/src/bootstrap/neode.js @@ -1,88 +1,9 @@ import Neode from 'neode' -import uuid from 'uuid/v4' +import models from '../models' export default function setupNeode(options) { const { uri, username, password } = options const neodeInstance = new Neode(uri, username, password) - neodeInstance.model('InvitationCode', { - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - token: { type: 'string', primary: true, token: true }, - generatedBy: { - type: 'relationship', - relationship: 'GENERATED', - target: 'User', - direction: 'in', - }, - activated: { - type: 'relationship', - relationship: 'ACTIVATED', - target: 'EmailAddress', - direction: 'out', - }, - }) - neodeInstance.model('EmailAddress', { - email: { type: 'string', primary: true, lowercase: true, email: true }, - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - verifiedAt: { type: 'string', isoDate: true }, - nonce: { type: 'string', token: true }, - belongsTo: { - type: 'relationship', - relationship: 'BELONGS_TO', - target: 'User', - direction: 'out', - }, - }) - neodeInstance.model('User', { - id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests - actorId: { type: 'string', allow: [null] }, - name: { type: 'string', min: 3 }, - email: { type: 'string', lowercase: true, email: true }, - slug: 'string', - encryptedPassword: 'string', - avatar: { type: 'string', allow: [null] }, - coverImg: { type: 'string', allow: [null] }, - deleted: { type: 'boolean', default: false }, - disabled: { type: 'boolean', default: false }, - role: 'string', - publicKey: 'string', - privateKey: 'string', - wasInvited: 'boolean', - wasSeeded: 'boolean', - locationName: { type: 'string', allow: [null] }, - about: { type: 'string', allow: [null] }, - primaryEmail: { - type: 'relationship', - relationship: 'PRIMARY_EMAIL', - target: 'EmailAddress', - direction: 'out', - }, - following: { - type: 'relationship', - relationship: 'FOLLOWS', - target: 'User', - direction: 'out', - }, - followedBy: { - type: 'relationship', - relationship: 'FOLLOWS', - target: 'User', - direction: 'in', - }, - friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, - invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' }, - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - updatedAt: { - type: 'string', - isoDate: true, - required: true, - default: () => new Date().toISOString(), - }, - }) + neodeInstance.with(models) return neodeInstance } diff --git a/backend/src/index.js b/backend/src/index.js index f28e58947..0e7fc233c 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,18 +1,8 @@ import createServer from './server' -import ActivityPub from './activitypub/ActivityPub' import CONFIG from './config' -const serverConfig = { - port: CONFIG.GRAPHQL_PORT, - // cors: { - // credentials: true, - // origin: [CONFIG.CLIENT_URI] // your frontend url. - // } -} - -const server = createServer() -server.start(serverConfig, options => { +const { app } = createServer() +app.listen({ port: CONFIG.GRAPHQL_PORT }, () => { /* eslint-disable-next-line no-console */ console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`) - ActivityPub.init(server) }) diff --git a/backend/src/jest/helpers.js b/backend/src/jest/helpers.js index e50f30c64..380aedd16 100644 --- a/backend/src/jest/helpers.js +++ b/backend/src/jest/helpers.js @@ -15,3 +15,9 @@ export async function login(variables) { authorization: `Bearer ${response.login}`, } } + +//* This is a fake ES2015 template string, just to benefit of syntax +// highlighting of `gql` template strings in certain editors. +export function gql(strings) { + return strings.join('') +} diff --git a/backend/src/middleware/email/templates/signup.js b/backend/src/middleware/email/templates/signup.js index 1a9c0de91..7751f0e67 100644 --- a/backend/src/middleware/email/templates/signup.js +++ b/backend/src/middleware/email/templates/signup.js @@ -11,6 +11,7 @@ export const signupTemplate = options => { } = options const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI) actionUrl.searchParams.set('nonce', nonce) + actionUrl.searchParams.set('email', email) return { to: email, diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.js b/backend/src/middleware/handleHtmlContent/handleContentData.js new file mode 100644 index 000000000..6519ddae7 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/handleContentData.js @@ -0,0 +1,69 @@ +import extractMentionedUsers from './notifications/extractMentionedUsers' +import extractHashtags from './hashtags/extractHashtags' + +const notify = async (postId, idsOfMentionedUsers, context) => { + const session = context.driver.session() + const createdAt = new Date().toISOString() + const cypher = ` + match(u:User) where u.id in $idsOfMentionedUsers + match(p:Post) where p.id = $postId + create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt}) + merge (n)-[:NOTIFIED]->(u) + merge (p)-[:NOTIFIED]->(n) + ` + await session.run(cypher, { + idsOfMentionedUsers, + createdAt, + postId, + }) + session.close() +} + +const updateHashtagsOfPost = async (postId, hashtags, context) => { + 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. + const cypherDeletePreviousRelations = ` + MATCH (p:Post { id: $postId })-[previousRelations:TAGGED]->(t:Tag) + DELETE previousRelations + RETURN p, t + ` + const cypherCreateNewTagsAndRelations = ` + MATCH (p:Post { id: $postId}) + UNWIND $hashtags AS tagName + MERGE (t:Tag { id: tagName, name: tagName, disabled: false, deleted: false }) + MERGE (p)-[:TAGGED]->(t) + RETURN p, t + ` + await session.run(cypherDeletePreviousRelations, { + postId, + }) + await session.run(cypherCreateNewTagsAndRelations, { + postId, + hashtags, + }) + session.close() +} + +const handleContentData = async (resolve, root, args, context, resolveInfo) => { + // extract user ids before xss-middleware removes classes via the following "resolve" call + const idsOfMentionedUsers = extractMentionedUsers(args.content) + // extract tag (hashtag) ids before xss-middleware removes classes via the following "resolve" call + const hashtags = extractHashtags(args.content) + + // removes classes from the content + const post = await resolve(root, args, context, resolveInfo) + + await notify(post.id, idsOfMentionedUsers, context) + await updateHashtagsOfPost(post.id, hashtags, context) + + return post +} + +export default { + Mutation: { + CreatePost: handleContentData, + UpdatePost: handleContentData, + }, +} diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js new file mode 100644 index 000000000..f2e3c2303 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js @@ -0,0 +1,285 @@ +import { GraphQLClient } from 'graphql-request' +import { host, login, gql } from '../../jest/helpers' +import Factory from '../../seed/factories' + +const factory = Factory() +let client + +beforeEach(async () => { + await factory.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + email: 'test@example.org', + password: '1234', + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('currentUser { notifications }', () => { + const query = gql` + query($read: Boolean) { + currentUser { + notifications(read: $read, orderBy: createdAt_desc) { + read + post { + content + } + } + } + } + ` + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { + headers, + }) + }) + + describe('given another user', () => { + let authorClient + let authorParams + let authorHeaders + + beforeEach(async () => { + authorParams = { + email: 'author@example.org', + password: '1234', + id: 'author', + } + await factory.create('User', authorParams) + authorHeaders = await login(authorParams) + }) + + describe('who mentions me in a post', () => { + let post + const title = 'Mentioning Al Capone' + const content = + 'Hey @al-capone how do you do?' + + beforeEach(async () => { + const createPostMutation = gql` + mutation($title: String!, $content: String!) { + CreatePost(title: $title, content: $content) { + id + title + content + } + } + ` + authorClient = new GraphQLClient(host, { + headers: authorHeaders, + }) + const { CreatePost } = await authorClient.request(createPostMutation, { + title, + content, + }) + post = CreatePost + }) + + it('sends you a notification', async () => { + const expectedContent = + 'Hey @al-capone how do you do?' + const expected = { + currentUser: { + notifications: [ + { + read: false, + post: { + content: expectedContent, + }, + }, + ], + }, + } + await expect( + client.request(query, { + read: false, + }), + ).resolves.toEqual(expected) + }) + + describe('who mentions me again', () => { + beforeEach(async () => { + const updatedContent = `${post.content} One more mention to @al-capone` + // The response `post.content` contains a link but the XSSmiddleware + // should have the `mention` CSS class removed. I discovered this + // during development and thought: A feature not a bug! This way we + // can encode a re-mentioning of users when you edit your post or + // comment. + const updatePostMutation = gql` + mutation($id: ID!, $title: String!, $content: String!) { + UpdatePost(id: $id, content: $content, title: $title) { + title + content + } + } + ` + authorClient = new GraphQLClient(host, { + headers: authorHeaders, + }) + await authorClient.request(updatePostMutation, { + id: post.id, + title: post.title, + content: updatedContent, + }) + }) + + it('creates exactly one more notification', async () => { + const expectedContent = + 'Hey @al-capone how do you do? One more mention to @al-capone' + const expected = { + currentUser: { + notifications: [ + { + read: false, + post: { + content: expectedContent, + }, + }, + { + read: false, + post: { + content: expectedContent, + }, + }, + ], + }, + } + await expect( + client.request(query, { + read: false, + }), + ).resolves.toEqual(expected) + }) + }) + }) + }) + }) +}) + +describe('Hashtags', () => { + const postId = 'p135' + const postTitle = 'Two Hashtags' + const postContent = + '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + const postWithHastagsQuery = gql` + query($id: ID) { + Post(id: $id) { + tags { + id + name + } + } + } + ` + const postWithHastagsVariables = { + id: postId, + } + const createPostMutation = gql` + mutation($postId: ID, $postTitle: String!, $postContent: String!) { + CreatePost(id: $postId, title: $postTitle, content: $postContent) { + id + title + content + } + } + ` + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { + headers, + }) + }) + + describe('create a Post with Hashtags', () => { + beforeEach(async () => { + await client.request(createPostMutation, { + postId, + postTitle, + postContent, + }) + }) + + it('both Hashtags are created with the "id" set to thier "name"', async () => { + const expected = [ + { + id: 'Democracy', + name: 'Democracy', + }, + { + id: 'Liberty', + name: 'Liberty', + }, + ] + await expect( + client.request(postWithHastagsQuery, postWithHastagsVariables), + ).resolves.toEqual({ + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], + }) + }) + + describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { + // The already existing Hashtag has no class at this point. + const updatedPostContent = + '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + const updatePostMutation = gql` + mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) { + UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) { + id + title + content + } + } + ` + + it('only one previous Hashtag and the new Hashtag exists', async () => { + await client.request(updatePostMutation, { + postId, + postTitle, + updatedPostContent, + }) + + const expected = [ + { + id: 'Elections', + name: 'Elections', + }, + { + id: 'Liberty', + name: 'Liberty', + }, + ] + await expect( + client.request(postWithHastagsQuery, postWithHastagsVariables), + ).resolves.toEqual({ + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], + }) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js new file mode 100644 index 000000000..fd6613065 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js @@ -0,0 +1,28 @@ +import cheerio from 'cheerio' +// formats of a Hashtag: +// https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style +// here: +// 0. Search for whole string. +// 1. Hashtag has only 'a-z', 'A-Z', and '0-9'. +// 2. If it starts with a digit '0-9' than 'a-z', or 'A-Z' has to follow. +const ID_REGEX = /^\/search\/hashtag\/(([a-zA-Z]+[a-zA-Z0-9]*)|([0-9]+[a-zA-Z]+[a-zA-Z0-9]*))$/g + +export default function(content) { + if (!content) return [] + const $ = cheerio.load(content) + // We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware. + // But we have to know, which Hashtags are removed from the content es well, so we search for the 'a' html-tag. + const urls = $('a') + .map((_, el) => { + return $(el).attr('href') + }) + .get() + const hashtags = [] + urls.forEach(url => { + let match + while ((match = ID_REGEX.exec(url)) != null) { + hashtags.push(match[1]) + } + }) + return hashtags +} diff --git a/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js new file mode 100644 index 000000000..eb581d8f5 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js @@ -0,0 +1,57 @@ +import extractHashtags from './extractHashtags' + +describe('extractHashtags', () => { + describe('content undefined', () => { + it('returns empty array', () => { + expect(extractHashtags()).toEqual([]) + }) + }) + + describe('searches through links', () => { + it('finds links with and without ".hashtag" class and extracts Hashtag names', () => { + const content = + '

#Elections#Democracy

' + expect(extractHashtags(content)).toEqual(['Elections', 'Democracy']) + }) + + it('ignores mentions', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractHashtags(content)).toEqual([]) + }) + + describe('handles links', () => { + it('ignores links with domains', () => { + const content = + '

#Elections#Democracy

' + expect(extractHashtags(content)).toEqual(['Democracy']) + }) + + it('ignores Hashtag links with not allowed character combinations', () => { + const content = + '

Something inspirational about #AbcDefXyz0123456789!*(),2, #0123456789, #0123456789a and #AbcDefXyz0123456789.

' + expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789']) + }) + }) + + describe('does not crash if', () => { + it('`href` contains no Hashtag name', () => { + const content = + '

Something inspirational about #Democracy and #liberty.

' + expect(extractHashtags(content)).toEqual([]) + }) + + it('`href` contains Hashtag as page anchor', () => { + const content = + '

Something inspirational about #anchor.

' + expect(extractHashtags(content)).toEqual([]) + }) + + it('`href` is empty or invalid', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractHashtags(content)).toEqual([]) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/extractIds/index.js b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js similarity index 100% rename from backend/src/middleware/notifications/extractIds/index.js rename to backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js diff --git a/backend/src/middleware/notifications/extractIds/spec.js b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js similarity index 79% rename from backend/src/middleware/notifications/extractIds/spec.js rename to backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js index 341c39cec..f39fbc859 100644 --- a/backend/src/middleware/notifications/extractIds/spec.js +++ b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js @@ -1,9 +1,9 @@ -import extractIds from '.' +import extractMentionedUsers from './extractMentionedUsers' -describe('extractIds', () => { +describe('extractMentionedUsers', () => { describe('content undefined', () => { it('returns empty array', () => { - expect(extractIds()).toEqual([]) + expect(extractMentionedUsers()).toEqual([]) }) }) @@ -11,33 +11,33 @@ describe('extractIds', () => { it('ignores links without .mention class', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) + expect(extractMentionedUsers(content)).toEqual([]) }) describe('given a link with .mention class', () => { it('extracts ids', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) + expect(extractMentionedUsers(content)).toEqual(['u2', 'u3']) }) describe('handles links', () => { it('with slug and id', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) + expect(extractMentionedUsers(content)).toEqual(['u2', 'u3']) }) it('with domains', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) + expect(extractMentionedUsers(content)).toEqual(['u2', 'u3']) }) it('special characters', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3']) + expect(extractMentionedUsers(content)).toEqual(['u!*(),2', 'u.~-3']) }) }) @@ -45,13 +45,13 @@ describe('extractIds', () => { it('`href` contains no user id', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) + expect(extractMentionedUsers(content)).toEqual([]) }) it('`href` is empty or invalid', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) + expect(extractMentionedUsers(content)).toEqual([]) }) }) }) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 14f85f91a..19c72c19f 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -1,4 +1,6 @@ +import { applyMiddleware } from 'graphql-middleware' import CONFIG from './../config' + import activityPub from './activityPubMiddleware' import softDelete from './softDeleteMiddleware' import sluggify from './sluggifyMiddleware' @@ -10,7 +12,7 @@ import user from './userMiddleware' import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' import validation from './validation/validationMiddleware' -import notifications from './notifications' +import handleContentData from './handleHtmlContent/handleContentData' import email from './email/emailMiddleware' export default schema => { @@ -21,7 +23,7 @@ export default schema => { validation: validation, sluggify: sluggify, excerpt: excerpt, - notifications: notifications, + handleContentData: handleContentData, xss: xss, softDelete: softDelete, user: user, @@ -38,7 +40,7 @@ export default schema => { 'sluggify', 'excerpt', 'email', - 'notifications', + 'handleContentData', 'xss', 'softDelete', 'user', @@ -56,5 +58,6 @@ export default schema => { console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`) } - return order.map(key => middlewares[key]) + const appliedMiddlewares = order.map(key => middlewares[key]) + return applyMiddleware(schema, ...appliedMiddlewares) } diff --git a/backend/src/middleware/notifications/index.js b/backend/src/middleware/notifications/index.js deleted file mode 100644 index ca460a512..000000000 --- a/backend/src/middleware/notifications/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import extractIds from './extractIds' - -const notify = async (resolve, root, args, context, resolveInfo) => { - // extract user ids before xss-middleware removes link classes - const ids = extractIds(args.content) - - const post = await resolve(root, args, context, resolveInfo) - - const session = context.driver.session() - const { id: postId } = post - const createdAt = new Date().toISOString() - const cypher = ` - match(u:User) where u.id in $ids - match(p:Post) where p.id = $postId - create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt}) - merge (n)-[:NOTIFIED]->(u) - merge (p)-[:NOTIFIED]->(n) - ` - await session.run(cypher, { ids, createdAt, postId }) - session.close() - - return post -} - -export default { - Mutation: { - CreatePost: notify, - UpdatePost: notify, - }, -} diff --git a/backend/src/middleware/notifications/spec.js b/backend/src/middleware/notifications/spec.js deleted file mode 100644 index d214a5571..000000000 --- a/backend/src/middleware/notifications/spec.js +++ /dev/null @@ -1,130 +0,0 @@ -import { GraphQLClient } from 'graphql-request' -import { host, login } from '../../jest/helpers' -import Factory from '../../seed/factories' - -const factory = Factory() -let client - -beforeEach(async () => { - await factory.create('User', { - id: 'you', - name: 'Al Capone', - slug: 'al-capone', - email: 'test@example.org', - password: '1234', - }) -}) - -afterEach(async () => { - await factory.cleanDatabase() -}) - -describe('currentUser { notifications }', () => { - const query = `query($read: Boolean) { - currentUser { - notifications(read: $read, orderBy: createdAt_desc) { - read - post { - content - } - } - } - }` - - describe('authenticated', () => { - let headers - beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - describe('given another user', () => { - let authorClient - let authorParams - let authorHeaders - - beforeEach(async () => { - authorParams = { - email: 'author@example.org', - password: '1234', - id: 'author', - } - await factory.create('User', authorParams) - authorHeaders = await login(authorParams) - }) - - describe('who mentions me in a post', () => { - let post - const title = 'Mentioning Al Capone' - const content = - 'Hey @al-capone how do you do?' - - beforeEach(async () => { - const createPostMutation = ` - mutation($title: String!, $content: String!) { - CreatePost(title: $title, content: $content) { - id - title - content - } - } - ` - authorClient = new GraphQLClient(host, { headers: authorHeaders }) - const { CreatePost } = await authorClient.request(createPostMutation, { title, content }) - post = CreatePost - }) - - it('sends you a notification', async () => { - const expectedContent = - 'Hey @al-capone how do you do?' - const expected = { - currentUser: { - notifications: [{ read: false, post: { content: expectedContent } }], - }, - } - await expect(client.request(query, { read: false })).resolves.toEqual(expected) - }) - - describe('who mentions me again', () => { - beforeEach(async () => { - const updatedContent = `${post.content} One more mention to @al-capone` - const updatedTitle = 'this post has been updated' - // The response `post.content` contains a link but the XSSmiddleware - // should have the `mention` CSS class removed. I discovered this - // during development and thought: A feature not a bug! This way we - // can encode a re-mentioning of users when you edit your post or - // comment. - const updatePostMutation = ` - mutation($id: ID!, $title: String!, $content: String!) { - UpdatePost(id: $id, title: $title, content: $content) { - title - content - } - } - ` - authorClient = new GraphQLClient(host, { headers: authorHeaders }) - await authorClient.request(updatePostMutation, { - id: post.id, - content: updatedContent, - title: updatedTitle, - }) - }) - - it('creates exactly one more notification', async () => { - const expectedContent = - 'Hey @al-capone how do you do? One more mention to @al-capone' - const expected = { - currentUser: { - notifications: [ - { read: false, post: { content: expectedContent } }, - { read: false, post: { content: expectedContent } }, - ], - }, - } - await expect(client.request(query, { read: false })).resolves.toEqual(expected) - }) - }) - }) - }) - }) -}) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index f1663ca2c..99f09d885 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -137,7 +137,7 @@ const permissions = shield( '*': deny, findPosts: allow, Category: allow, - Tag: isAdmin, + Tag: allow, Report: isModerator, Notification: isAdmin, statistics: allow, @@ -146,6 +146,7 @@ const permissions = shield( Comment: allow, User: or(noEmailFilter, isAdmin), isLoggedIn: allow, + Badge: allow, }, Mutation: { '*': deny, @@ -160,9 +161,6 @@ const permissions = shield( UpdatePost: isAuthor, DeletePost: isAuthor, report: isAuthenticated, - CreateBadge: isAdmin, - UpdateBadge: isAdmin, - DeleteBadge: isAdmin, CreateSocialMedia: isAuthenticated, UpdateSocialMedia: isAuthenticated, DeleteSocialMedia: isAuthenticated, @@ -179,6 +177,7 @@ const permissions = shield( enable: isModerator, disable: isModerator, CreateComment: isAuthenticated, + UpdateComment: isAuthor, DeleteComment: isAuthor, DeleteUser: isDeletingOwnAccount, requestPasswordReset: allow, diff --git a/backend/src/middleware/validation/index.js b/backend/src/middleware/validation/index.js deleted file mode 100644 index ca7a6b338..000000000 --- a/backend/src/middleware/validation/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { UserInputError } from 'apollo-server' - -const validateUrl = async (resolve, root, args, context, info) => { - const { url } = args - const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g) - if (isValid) { - /* eslint-disable-next-line no-return-await */ - return await resolve(root, args, context, info) - } else { - throw new UserInputError('Input is not a URL') - } -} - -export default { - Mutation: { - CreateSocialMedia: validateUrl, - }, -} diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 9ac15a60f..319a9a6c0 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -1,6 +1,9 @@ import { UserInputError } from 'apollo-server' import Joi from '@hapi/joi' +const COMMENT_MIN_LENGTH = 1 +const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' + const validate = schema => { return async (resolve, root, args, context, info) => { const validation = schema.validate(args) @@ -15,8 +18,47 @@ const socialMediaSchema = Joi.object().keys({ .required(), }) +const validateCommentCreation = async (resolve, root, args, context, info) => { + const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() + const { postId } = args + + if (!args.content || content.length < COMMENT_MIN_LENGTH) { + throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) + } + const session = context.driver.session() + const postQueryRes = await session.run( + ` + MATCH (post:Post {id: $postId}) + RETURN 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) + } +} + +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!`) + } + + return resolve(root, args, context, info) +} + export default { Mutation: { CreateSocialMedia: validate(socialMediaSchema), + CreateComment: validateCommentCreation, + UpdateComment: validateUpdateComment, }, } diff --git a/backend/src/models/Badge.js b/backend/src/models/Badge.js new file mode 100644 index 000000000..6968a056b --- /dev/null +++ b/backend/src/models/Badge.js @@ -0,0 +1,7 @@ +module.exports = { + id: { type: 'string', primary: true, lowercase: true }, + status: { type: 'string', valid: ['permanent', 'temporary'] }, + type: { type: 'string', valid: ['role', 'crowdfunding'] }, + icon: { type: 'string', required: true }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, +} diff --git a/backend/src/models/EmailAddress.js b/backend/src/models/EmailAddress.js new file mode 100644 index 000000000..6afccd1ed --- /dev/null +++ b/backend/src/models/EmailAddress.js @@ -0,0 +1,13 @@ +module.exports = { + email: { type: 'string', primary: true, lowercase: true, email: true }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + verifiedAt: { type: 'string', isoDate: true }, + nonce: { type: 'string', token: true }, + belongsTo: { + type: 'relationship', + relationship: 'BELONGS_TO', + target: 'User', + direction: 'out', + eager: true, + }, +} diff --git a/backend/src/models/InvitationCode.js b/backend/src/models/InvitationCode.js new file mode 100644 index 000000000..f137f6c15 --- /dev/null +++ b/backend/src/models/InvitationCode.js @@ -0,0 +1,16 @@ +module.exports = { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + token: { type: 'string', primary: true, token: true }, + generatedBy: { + type: 'relationship', + relationship: 'GENERATED', + target: 'User', + direction: 'in', + }, + activated: { + type: 'relationship', + relationship: 'ACTIVATED', + target: 'EmailAddress', + direction: 'out', + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js new file mode 100644 index 000000000..cac8fd7a6 --- /dev/null +++ b/backend/src/models/User.js @@ -0,0 +1,59 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests + actorId: { type: 'string', allow: [null] }, + name: { type: 'string', min: 3 }, + slug: 'string', + encryptedPassword: 'string', + avatar: { type: 'string', allow: [null] }, + coverImg: { type: 'string', allow: [null] }, + deleted: { type: 'boolean', default: false }, + disabled: { type: 'boolean', default: false }, + role: { type: 'string', default: 'user' }, + publicKey: 'string', + privateKey: 'string', + wasInvited: 'boolean', + wasSeeded: 'boolean', + locationName: { type: 'string', allow: [null] }, + about: { type: 'string', allow: [null] }, + primaryEmail: { + type: 'relationship', + relationship: 'PRIMARY_EMAIL', + target: 'EmailAddress', + direction: 'out', + }, + following: { + type: 'relationship', + relationship: 'FOLLOWS', + target: 'User', + direction: 'out', + }, + followedBy: { + type: 'relationship', + relationship: 'FOLLOWS', + target: 'User', + direction: 'in', + }, + friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' }, + disabledBy: { + type: 'relationship', + relationship: 'DISABLED', + target: 'User', + direction: 'in', + }, + rewarded: { + type: 'relationship', + relationship: 'REWARDED', + target: 'Badge', + direction: 'in', + }, + invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { + type: 'string', + isoDate: true, + required: true, + default: () => new Date().toISOString(), + }, +} diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js new file mode 100644 index 000000000..e00136970 --- /dev/null +++ b/backend/src/models/User.spec.js @@ -0,0 +1,20 @@ +import Factory from '../seed/factories' +import { neode } from '../bootstrap/neo4j' + +const factory = Factory() +const instance = neode() + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('role', () => { + it('defaults to `user`', async () => { + const user = await instance.create('User', { name: 'John' }) + await expect(user.toJson()).resolves.toEqual( + expect.objectContaining({ + role: 'user', + }), + ) + }) +}) diff --git a/backend/src/models/index.js b/backend/src/models/index.js new file mode 100644 index 000000000..09d1dbbeb --- /dev/null +++ b/backend/src/models/index.js @@ -0,0 +1,8 @@ +// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm +// module that is not browser-compatible. Node's `fs` module is server-side only +export default { + Badge: require('./Badge.js'), + User: require('./User.js'), + InvitationCode: require('./InvitationCode.js'), + EmailAddress: require('./EmailAddress.js'), +} diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 8fbb5cfda..0f724d9b5 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -12,11 +12,25 @@ export default applyScalars( resolvers, config: { query: { - exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'], + exclude: [ + 'Badge', + 'InvitationCode', + 'EmailAddress', + 'Notfication', + 'Statistics', + 'LoggedInUser', + ], // add 'User' here as soon as possible }, mutation: { - exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'], + exclude: [ + 'Badge', + 'InvitationCode', + 'EmailAddress', + 'Notfication', + 'Statistics', + 'LoggedInUser', + ], // add 'User' here as soon as possible }, debug: CONFIG.DEBUG, diff --git a/backend/src/schema/resolvers/badges.js b/backend/src/schema/resolvers/badges.js new file mode 100644 index 000000000..19bc24fd6 --- /dev/null +++ b/backend/src/schema/resolvers/badges.js @@ -0,0 +1,9 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Query: { + Badge: async (object, args, context, resolveInfo) => { + return neo4jgraphql(object, args, context, resolveInfo, false) + }, + }, +} diff --git a/backend/src/schema/resolvers/badges.spec.js b/backend/src/schema/resolvers/badges.spec.js deleted file mode 100644 index a0dbafe00..000000000 --- a/backend/src/schema/resolvers/badges.spec.js +++ /dev/null @@ -1,200 +0,0 @@ -import { GraphQLClient } from 'graphql-request' -import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' - -const factory = Factory() -let client - -describe('badges', () => { - beforeEach(async () => { - await factory.create('User', { - email: 'user@example.org', - role: 'user', - password: '1234', - }) - await factory.create('User', { - id: 'u2', - role: 'moderator', - email: 'moderator@example.org', - }) - await factory.create('User', { - id: 'u3', - role: 'admin', - email: 'admin@example.org', - }) - }) - - afterEach(async () => { - await factory.cleanDatabase() - }) - - describe('CreateBadge', () => { - const variables = { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', - icon: '/img/badges/indiegogo_en_racoon.svg', - } - - const mutation = ` - mutation( - $id: ID - $key: String! - $type: BadgeType! - $status: BadgeStatus! - $icon: String! - ) { - CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { - id, - key, - type, - status, - icon - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - const headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('creates a badge', async () => { - const expected = { - CreateBadge: { - icon: '/img/badges/indiegogo_en_racoon.svg', - id: 'b1', - key: 'indiegogo_en_racoon', - status: 'permanent', - type: 'crowdfunding', - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - }) - - describe('UpdateBadge', () => { - beforeEach(async () => { - await factory.authenticateAs({ email: 'admin@example.org', password: '1234' }) - await factory.create('Badge', { id: 'b1' }) - }) - const variables = { - id: 'b1', - key: 'whatever', - } - - const mutation = ` - mutation($id: ID!, $key: String!) { - UpdateBadge(id: $id, key: $key) { - id - key - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - const headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('updates a badge', async () => { - const expected = { - UpdateBadge: { - id: 'b1', - key: 'whatever', - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) - }) - }) - }) - - describe('DeleteBadge', () => { - beforeEach(async () => { - await factory.authenticateAs({ email: 'admin@example.org', password: '1234' }) - await factory.create('Badge', { id: 'b1' }) - }) - const variables = { - id: 'b1', - } - - const mutation = ` - mutation($id: ID!) { - DeleteBadge(id: $id) { - id - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - const headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('deletes a badge', async () => { - const expected = { - DeleteBadge: { - id: 'b1', - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 7aef63c59..31219d6d9 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -1,40 +1,15 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -import { UserInputError } from 'apollo-server' - -const COMMENT_MIN_LENGTH = 1 -const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' export default { Mutation: { CreateComment: async (object, params, context, resolveInfo) => { - const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim() const { postId } = params // Adding relationship from comment to post by passing in the postId, // but we do not want to create the comment with postId as an attribute // because we use relationships for this. So, we are deleting it from params // before comment creation. delete params.postId - - if (!params.content || content.length < COMMENT_MIN_LENGTH) { - throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) - } - const session = context.driver.session() - const postQueryRes = await session.run( - ` - MATCH (post:Post {id: $postId}) - RETURN post`, - { - postId, - }, - ) - const [post] = postQueryRes.records.map(record => { - return record.get('post') - }) - - if (!post) { - throw new UserInputError(NO_POST_ERR_MESSAGE) - } const commentWithoutRelationships = await neo4jgraphql( object, params, diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index 7f17539dc..890ba5feb 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -1,7 +1,6 @@ -import gql from 'graphql-tag' import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() let client @@ -10,7 +9,28 @@ let createPostVariables let createCommentVariablesSansPostId let createCommentVariablesWithNonExistentPost let userParams -let authorParams +let headers + +const createPostMutation = gql` + mutation($id: ID!, $title: String!, $content: String!) { + CreatePost(id: $id, title: $title, content: $content) { + id + } + } +` +const createCommentMutation = gql` + mutation($id: ID, $postId: ID!, $content: String!) { + CreateComment(id: $id, postId: $postId, content: $content) { + id + content + } + } +` +createPostVariables = { + id: 'p1', + title: 'post to comment on', + content: 'please comment on me', +} beforeEach(async () => { userParams = { @@ -26,21 +46,6 @@ afterEach(async () => { }) describe('CreateComment', () => { - const createCommentMutation = gql` - mutation($postId: ID!, $content: String!) { - CreateComment(postId: $postId, content: $content) { - id - content - } - } - ` - const createPostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { - id - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { createCommentVariables = { @@ -55,7 +60,6 @@ describe('CreateComment', () => { }) describe('authenticated', () => { - let headers beforeEach(async () => { headers = await login(userParams) client = new GraphQLClient(host, { @@ -65,11 +69,6 @@ describe('CreateComment', () => { postId: 'p1', content: "I'm authorised to comment", } - createPostVariables = { - id: 'p1', - title: 'post to comment on', - content: 'please comment on me', - } await client.request(createPostMutation, createPostVariables) }) @@ -188,19 +187,8 @@ describe('CreateComment', () => { }) }) -describe('DeleteComment', () => { - const deleteCommentMutation = gql` - mutation($id: ID!) { - DeleteComment(id: $id) { - id - } - } - ` - - let deleteCommentVariables = { - id: 'c1', - } - +describe('ManageComments', () => { + let authorParams beforeEach(async () => { authorParams = { email: 'author@example.org', @@ -214,51 +202,178 @@ describe('DeleteComment', () => { content: 'Post to be commented', }) await asAuthor.create('Comment', { - id: 'c1', + id: 'c456', postId: 'p1', content: 'Comment to be deleted', }) }) - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( - 'Not Authorised', - ) - }) - }) - - describe('authenticated but not the author', () => { - beforeEach(async () => { - let headers - headers = await login(userParams) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( - 'Not Authorised', - ) - }) - }) - - describe('authenticated as author', () => { - beforeEach(async () => { - let headers - headers = await login(authorParams) - client = new GraphQLClient(host, { headers }) - }) - - it('deletes the comment', async () => { - const expected = { - DeleteComment: { - id: 'c1', - }, + describe('UpdateComment', () => { + const updateCommentMutation = gql` + mutation($content: String!, $id: ID!) { + UpdateComment(content: $content, id: $id) { + id + content + } } - await expect(client.request(deleteCommentMutation, deleteCommentVariables)).resolves.toEqual( - expected, - ) + ` + + let updateCommentVariables = { + id: 'c456', + content: 'The comment is updated', + } + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated but not the author', () => { + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('throws authorization error', async () => { + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated as author', () => { + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('updates the comment', async () => { + const expected = { + UpdateComment: { + id: 'c456', + content: 'The comment is updated', + }, + } + await expect( + client.request(updateCommentMutation, updateCommentVariables), + ).resolves.toEqual(expected) + }) + + it('throw an error if an empty string is sent from the editor as content', async () => { + updateCommentVariables = { + id: 'c456', + content: '

', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Comment must be at least 1 character long!', + ) + }) + + it('throws an error if a comment sent from the editor does not contain a single letter character', async () => { + updateCommentVariables = { + id: 'c456', + content: '

', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Comment must be at least 1 character long!', + ) + }) + + it('throws an error if commentId is sent as an empty string', async () => { + updateCommentVariables = { + id: '', + content: '

Hello

', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised!', + ) + }) + + it('throws an error if the comment does not exist in the database', async () => { + updateCommentVariables = { + id: 'c1000', + content: '

Hello

', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised!', + ) + }) + }) + }) + + describe('DeleteComment', () => { + const deleteCommentMutation = gql` + mutation($id: ID!) { + DeleteComment(id: $id) { + id + } + } + ` + + let deleteCommentVariables = { + id: 'c456', + } + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated but not the author', () => { + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('throws authorization error', async () => { + await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated as author', () => { + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('deletes the comment', async () => { + const expected = { + DeleteComment: { + id: 'c456', + }, + } + await expect( + client.request(deleteCommentMutation, deleteCommentVariables), + ).resolves.toEqual(expected) + }) }) }) }) diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index 415eb6f21..88d82846a 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -5,7 +5,7 @@ export async function createPasswordReset(options) { const { driver, code, email, issuedAt = new Date() } = options const session = driver.session() const cypher = ` - MATCH (u:User) WHERE u.email = $email + MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL}) MERGE (u)-[:REQUESTED]->(pr) RETURN u @@ -35,7 +35,7 @@ export default { const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) const cypher = ` MATCH (pr:PasswordReset {code: $code}) - MATCH (u:User {email: $email})-[:REQUESTED]->(pr) + MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr) WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL SET pr.usedAt = datetime() SET u.encryptedPassword = $encryptedNewPassword diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 2e5069de7..233be450a 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -18,10 +18,11 @@ const createPostWithCategoriesMutation = ` mutation($title: String!, $content: String!, $categoryIds: [ID]) { CreatePost(title: $title, content: $content, categoryIds: $categoryIds) { id + title } } ` -const creatPostWithCategoriesVariables = { +const createPostWithCategoriesVariables = { title: postTitle, content: postContent, categoryIds: ['cat9', 'cat4', 'cat15'], @@ -35,6 +36,26 @@ const postQueryWithCategories = ` } } ` +const createPostWithoutCategoriesVariables = { + title: 'This is a post without categories', + content: 'I should be able to filter it out', + categoryIds: null, +} +const postQueryFilteredByCategory = ` +query Post($filter: _PostFilter) { + Post(filter: $filter) { + title + id + categories { + id + } + } + } +` +const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } } +const postQueryFilteredByCategoryVariables = { + filter: postCategoriesFilterParam, +} beforeEach(async () => { userParams = { name: 'TestUser', @@ -133,7 +154,8 @@ describe('CreatePost', () => { }) describe('categories', () => { - it('allows a user to set the categories of the post', async () => { + let postWithCategories + beforeEach(async () => { await Promise.all([ factory.create('Category', { id: 'cat9', @@ -151,18 +173,39 @@ describe('CreatePost', () => { icon: 'shopping-cart', }), ]) - const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }] - const postWithCategories = await client.request( + postWithCategories = await client.request( createPostWithCategoriesMutation, - creatPostWithCategoriesVariables, + createPostWithCategoriesVariables, ) + }) + + it('allows a user to set the categories of the post', async () => { + const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }] const postQueryWithCategoriesVariables = { id: postWithCategories.CreatePost.id, } + await expect( client.request(postQueryWithCategories, postQueryWithCategoriesVariables), ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] }) }) + + it('allows a user to filter for posts by category', async () => { + await client.request(createPostWithCategoriesMutation, createPostWithoutCategoriesVariables) + const categoryIds = [{ id: 'cat4' }, { id: 'cat15' }, { id: 'cat9' }] + const expected = { + Post: [ + { + title: postTitle, + id: postWithCategories.CreatePost.id, + categories: expect.arrayContaining(categoryIds), + }, + ], + } + await expect( + client.request(postQueryFilteredByCategory, postQueryFilteredByCategoryVariables), + ).resolves.toEqual(expected) + }) }) }) }) @@ -260,7 +303,7 @@ describe('UpdatePost', () => { ]) postWithCategories = await client.request( createPostWithCategoriesMutation, - creatPostWithCategoriesVariables, + createPostWithCategoriesVariables, ) updatePostVariables = { id: postWithCategories.CreatePost.id, diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index 3c8243d8a..20c54a49b 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -12,8 +12,8 @@ const instance = neode() */ const checkEmailDoesNotExist = async ({ email }) => { email = email.toLowerCase() - const users = await instance.all('User', { email }) - if (users.length > 0) throw new UserInputError('User account with this email already exists.') + const emails = await instance.all('EmailAddress', { email }) + if (emails.length > 0) throw new UserInputError('User account with this email already exists.') } export default { diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 2cbce9a36..dc2e96348 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -166,11 +166,12 @@ describe('SignupByInvitation', () => { await expect(action()).rejects.toThrow('"email" must be a valid email') }) - it('creates no EmailAddress node', async done => { + it('creates no additional EmailAddress node', async done => { try { await action() } catch (e) { - const emailAddresses = await instance.all('EmailAddress') + let emailAddresses = await instance.all('EmailAddress') + emailAddresses = await emailAddresses.toJson expect(emailAddresses).toHaveLength(0) done() } @@ -191,16 +192,16 @@ describe('SignupByInvitation', () => { describe('creates a EmailAddress node', () => { it('with a `createdAt` attribute', async () => { await action() - const emailAddresses = await instance.all('EmailAddress') - const emailAddress = await emailAddresses.first().toJson() + let emailAddress = await instance.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 action() - const emailAddresses = await instance.all('EmailAddress') - const emailAddress = await emailAddresses.first().toJson() + let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.toJson() expect(emailAddress.nonce).toEqual(expect.any(String)) }) @@ -220,6 +221,7 @@ describe('SignupByInvitation', () => { it('rejects because codes can be used only once', async done => { await action() try { + variables.email = 'yetanotheremail@example.org' await action() } catch (e) { expect(e.message).toMatch(/Invitation code already used/) @@ -282,8 +284,8 @@ describe('Signup', () => { it('creates a Signup with a cryptographic `nonce`', async () => { await action() - const emailAddresses = await instance.all('EmailAddress') - const emailAddress = await emailAddresses.first().toJson() + let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.toJson() expect(emailAddress.nonce).toEqual(expect.any(String)) }) }) diff --git a/backend/src/schema/resolvers/rewards.js b/backend/src/schema/resolvers/rewards.js index ec5043da3..f7a759aa4 100644 --- a/backend/src/schema/resolvers/rewards.js +++ b/backend/src/schema/resolvers/rewards.js @@ -1,47 +1,47 @@ +import { neode } from '../../bootstrap/neo4j' +import { UserInputError } from 'apollo-server' + +const instance = neode() + +const getUserAndBadge = async ({ badgeKey, userId }) => { + let user = await instance.first('User', 'id', userId) + const badge = await instance.first('Badge', 'id', badgeKey) + if (!user) throw new UserInputError("Couldn't find a user with that id") + if (!badge) throw new UserInputError("Couldn't find a badge with that id") + return { user, badge } +} + export default { Mutation: { reward: async (_object, params, context, _resolveInfo) => { - const { fromBadgeId, toUserId } = params - const session = context.driver.session() - - let transactionRes = await session.run( - `MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId}) - MERGE (badge)-[:REWARDED]->(rewardedUser) - RETURN rewardedUser {.id}`, - { - badgeId: fromBadgeId, - rewardedUserId: toUserId, - }, - ) - - const [rewardedUser] = transactionRes.records.map(record => { - return record.get('rewardedUser') - }) - - session.close() - - return rewardedUser.id + const { user, badge } = await getUserAndBadge(params) + await user.relateTo(badge, 'rewarded') + return user.toJson() }, unreward: async (_object, params, context, _resolveInfo) => { - const { fromBadgeId, toUserId } = params + const { badgeKey, userId } = params + const { user } = await getUserAndBadge(params) const session = context.driver.session() - - let transactionRes = await session.run( - `MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId}) - DELETE reward - RETURN rewardedUser {.id}`, - { - badgeId: fromBadgeId, - rewardedUserId: toUserId, - }, - ) - const [rewardedUser] = transactionRes.records.map(record => { - return record.get('rewardedUser') - }) - session.close() - - return rewardedUser.id + try { + // silly neode cannot remove relationships + await session.run( + ` + MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) + DELETE reward + RETURN rewardedUser + `, + { + badgeKey, + userId, + }, + ) + } catch (err) { + throw err + } finally { + session.close() + } + return user.toJson() }, }, } diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index 2bdd9a39b..3b94e93aa 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,12 +1,19 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() +let user +let badge describe('rewards', () => { + const variables = { + from: 'indiegogo_en_rhino', + to: 'u1', + } + beforeEach(async () => { - await factory.create('User', { + user = await factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', @@ -22,9 +29,8 @@ describe('rewards', () => { role: 'admin', email: 'admin@example.org', }) - await factory.create('Badge', { - id: 'b6', - key: 'indiegogo_en_rhino', + badge = await factory.create('Badge', { + id: 'indiegogo_en_rhino', type: 'crowdfunding', status: 'permanent', icon: '/img/badges/indiegogo_en_rhino.svg', @@ -35,21 +41,19 @@ describe('rewards', () => { await factory.cleanDatabase() }) - describe('RewardBadge', () => { - const mutation = ` - mutation( - $from: ID! - $to: ID! - ) { - reward(fromBadgeId: $from, toUserId: $to) + describe('reward', () => { + const mutation = gql` + mutation($from: ID!, $to: ID!) { + reward(badgeKey: $from, userId: $to) { + id + badges { + id + } + } } ` describe('unauthenticated', () => { - const variables = { - from: 'b6', - to: 'u1', - } let client it('throws authorization error', async () => { @@ -65,74 +69,95 @@ describe('rewards', () => { client = new GraphQLClient(host, { headers }) }) + describe('badge for id does not exist', () => { + it('rejects with a telling error message', async () => { + await expect( + client.request(mutation, { + ...variables, + from: 'bullshit', + }), + ).rejects.toThrow("Couldn't find a badge with that id") + }) + }) + + describe('user for id does not exist', () => { + it('rejects with a telling error message', async () => { + await expect( + client.request(mutation, { + ...variables, + to: 'bullshit', + }), + ).rejects.toThrow("Couldn't find a user with that id") + }) + }) + it('rewards a badge to user', async () => { - const variables = { - from: 'b6', - to: 'u1', - } const expected = { - reward: 'u1', + reward: { + id: 'u1', + badges: [{ id: 'indiegogo_en_rhino' }], + }, } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) + it('rewards a second different badge to same user', async () => { await factory.create('Badge', { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_racoon', icon: '/img/badges/indiegogo_en_racoon.svg', }) - const variables = { - from: 'b1', - to: 'u1', - } + const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }] const expected = { - reward: 'u1', + reward: { + id: 'u1', + badges: expect.arrayContaining(badges), + }, } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + await client.request(mutation, variables) + await expect( + client.request(mutation, { + ...variables, + from: 'indiegogo_en_racoon', + }), + ).resolves.toEqual(expected) }) + it('rewards the same badge as well to another user', async () => { - const variables1 = { - from: 'b6', - to: 'u1', - } - await client.request(mutation, variables1) - - const variables2 = { - from: 'b6', - to: 'u2', - } const expected = { - reward: 'u2', + reward: { + id: 'u2', + badges: [{ id: 'indiegogo_en_rhino' }], + }, } - await expect(client.request(mutation, variables2)).resolves.toEqual(expected) + await expect( + client.request(mutation, { + ...variables, + to: 'u2', + }), + ).resolves.toEqual(expected) }) - it('returns the original reward if a reward is attempted a second time', async () => { - const variables = { - from: 'b6', - to: 'u1', - } + + it('creates no duplicate reward relationships', async () => { await client.request(mutation, variables) await client.request(mutation, variables) - const query = `{ - User( id: "u1" ) { - badgesCount + const query = gql` + { + User(id: "u1") { + badgesCount + badges { + id + } + } } - } ` - const expected = { User: [{ badgesCount: 1 }] } + const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] } await expect(client.request(query)).resolves.toEqual(expected) }) }) describe('authenticated moderator', () => { - const variables = { - from: 'b6', - to: 'u1', - } let client beforeEach(async () => { const headers = await login({ email: 'moderator@example.org', password: '1234' }) @@ -147,27 +172,41 @@ describe('rewards', () => { }) }) - describe('RemoveReward', () => { + describe('unreward', () => { beforeEach(async () => { - await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' }) + await user.relateTo(badge, 'rewarded') }) - const variables = { - from: 'b6', - to: 'u1', - } - const expected = { - unreward: 'u1', - } + const expected = { unreward: { id: 'u1', badges: [] } } - const mutation = ` - mutation( - $from: ID! - $to: ID! - ) { - unreward(fromBadgeId: $from, toUserId: $to) + const mutation = gql` + mutation($from: ID!, $to: ID!) { + unreward(badgeKey: $from, userId: $to) { + id + badges { + id + } + } } ` + describe('check test setup', () => { + it('user has one badge', async () => { + const query = gql` + { + User(id: "u1") { + badgesCount + badges { + id + } + } + } + ` + const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] } + const client = new GraphQLClient(host) + await expect(client.request(query)).resolves.toEqual(expected) + }) + }) + describe('unauthenticated', () => { let client @@ -188,12 +227,9 @@ describe('rewards', () => { await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) - it('fails to remove a not existing badge from user', async () => { + it('does not crash when unrewarding multiple times', async () => { await client.request(mutation, variables) - - await expect(client.request(mutation, variables)).rejects.toThrow( - "Cannot read property 'id' of undefined", - ) + await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) }) diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index 176d6e5da..91224d889 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -1,7 +1,6 @@ -import gql from 'graphql-tag' import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index b62f9a609..7ed84586b 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -2,6 +2,9 @@ import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' import { neo4jgraphql } from 'neo4j-graphql-js' +import { neode } from '../../bootstrap/neo4j' + +const instance = neode() export default { Query: { @@ -21,8 +24,8 @@ export default { // } const session = driver.session() const result = await session.run( - 'MATCH (user:User {email: $userEmail}) ' + - 'RETURN user {.id, .slug, .name, .avatar, .email, .encryptedPassword, .role, .disabled} as user LIMIT 1', + 'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' + + 'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1', { userEmail: email, }, @@ -46,41 +49,24 @@ export default { } }, changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { - const session = driver.session() - let result = await session.run( - `MATCH (user:User {email: $userEmail}) - RETURN user {.id, .email, .encryptedPassword}`, - { - userEmail: user.email, - }, - ) + let currentUser = await instance.find('User', user.id) - const [currentUser] = result.records.map(function(record) { - return record.get('user') - }) - - if (!(await bcrypt.compareSync(oldPassword, currentUser.encryptedPassword))) { + const encryptedPassword = currentUser.get('encryptedPassword') + if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { throw new AuthenticationError('Old password is not correct') } - if (await bcrypt.compareSync(newPassword, currentUser.encryptedPassword)) { + if (await bcrypt.compareSync(newPassword, encryptedPassword)) { throw new AuthenticationError('Old password and new password should be different') - } else { - const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) - session.run( - `MATCH (user:User {email: $userEmail}) - SET user.encryptedPassword = $newEncryptedPassword - RETURN user - `, - { - userEmail: user.email, - newEncryptedPassword, - }, - ) - session.close() - - return encode(currentUser) } + + const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) + await currentUser.update({ + encryptedPassword: newEncryptedPassword, + updatedAt: new Date().toISOString(), + }) + + return encode(await currentUser.toJson()) }, }, } diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 2d9282b60..820688a1a 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -65,6 +65,13 @@ export const hasOne = obj => { export default { Query: { User: async (object, args, context, resolveInfo) => { + const { email } = args + if (email) { + const e = await instance.first('EmailAddress', { email }) + let user = e.get('belongsTo') + user = await user.toJson() + return [user.node] + } return neo4jgraphql(object, args, context, resolveInfo, false) }, }, @@ -104,6 +111,14 @@ export default { }, }, User: { + email: async (parent, params, context, resolveInfo) => { + if (typeof parent.email !== 'undefined') return parent.email + const { id } = parent + const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e` + const result = await instance.cypher(statement, { id }) + let [{ email }] = result.records.map(r => r.get('e').properties) + return email + }, ...undefinedToNull([ 'actorId', 'avatar', @@ -139,7 +154,7 @@ export default { organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)', organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)', categories: '-[:CATEGORIZED]->(related:Category)', - badges: '-[:REWARDED]->(related:Badge)', + badges: '<-[:REWARDED]-(related:Badge)', }), }, } diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 6f9b6dd3d..fff6acadb 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,7 +1,6 @@ import { GraphQLClient } from 'graphql-request' -import { login, host } from '../../jest/helpers' import Factory from '../../seed/factories' -import gql from 'graphql-tag' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() let client @@ -147,7 +146,7 @@ describe('users', () => { } ` beforeEach(async () => { - asAuthor = await factory.create('User', { + await factory.create('User', { email: 'test@example.org', password: '1234', id: 'u343', @@ -191,6 +190,7 @@ describe('users', () => { describe('attempting to delete my own account', () => { let expectedResponse beforeEach(async () => { + asAuthor = Factory() await asAuthor.authenticateAs({ email: 'test@example.org', password: '1234', diff --git a/backend/src/schema/types/enum/BadgeStatus.gql b/backend/src/schema/types/enum/BadgeStatus.gql deleted file mode 100644 index b109663b3..000000000 --- a/backend/src/schema/types/enum/BadgeStatus.gql +++ /dev/null @@ -1,4 +0,0 @@ -enum BadgeStatus { - permanent - temporary -} \ No newline at end of file diff --git a/backend/src/schema/types/enum/BadgeType.gql b/backend/src/schema/types/enum/BadgeType.gql deleted file mode 100644 index eccf2e661..000000000 --- a/backend/src/schema/types/enum/BadgeType.gql +++ /dev/null @@ -1,4 +0,0 @@ -enum BadgeType { - role - crowdfunding -} \ No newline at end of file diff --git a/backend/src/schema/types/enum/Emotion.gql b/backend/src/schema/types/enum/Emotion.gql new file mode 100644 index 000000000..88a436f98 --- /dev/null +++ b/backend/src/schema/types/enum/Emotion.gql @@ -0,0 +1,7 @@ +enum Emotion { + surprised + cry + happy + angry + funny +} \ No newline at end of file diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 261501600..492dd3966 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -28,8 +28,6 @@ type Mutation { report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID - reward(fromBadgeId: ID!, toUserId: ID!): ID - unreward(fromBadgeId: ID!, toUserId: 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/schema_full.gql_ b/backend/src/schema/types/schema_full.gql_ deleted file mode 100644 index a581d287c..000000000 --- a/backend/src/schema/types/schema_full.gql_ +++ /dev/null @@ -1,324 +0,0 @@ -scalar Upload - -type Query { - isLoggedIn: Boolean! - # Get the currently logged in User based on the given JWT Token - currentUser: User - # Get the latest Network Statistics - statistics: Statistics! - findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( - statement: """ - CALL db.index.fulltext.queryNodes('full_text_search', $filter) - YIELD node as post, score - MATCH (post)<-[:WROTE]-(user:User) - WHERE score >= 0.2 - AND NOT user.deleted = true AND NOT user.disabled = true - AND NOT post.deleted = true AND NOT post.disabled = true - RETURN post - LIMIT $limit - """ - ) - CommentByPost(postId: ID!): [Comment]! -} - -type Mutation { - # Get a JWT Token for the given Email and password - login(email: String!, password: String!): String! - signup(email: String!, password: String!): Boolean! - changePassword(oldPassword:String!, newPassword: String!): String! - report(id: ID!, description: String): Report - disable(id: ID!): ID - enable(id: ID!): ID - reward(fromBadgeId: ID!, toUserId: ID!): ID - unreward(fromBadgeId: ID!, toUserId: ID!): ID - # Shout the given Type and ID - shout(id: ID!, type: ShoutTypeEnum): Boolean! - # Unshout the given Type and ID - unshout(id: ID!, type: ShoutTypeEnum): Boolean! - # Follow the given Type and ID - follow(id: ID!, type: FollowTypeEnum): Boolean! - # Unfollow the given Type and ID - unfollow(id: ID!, type: FollowTypeEnum): Boolean! -} - -type Statistics { - countUsers: Int! - countPosts: Int! - countComments: Int! - countNotifications: Int! - countOrganizations: Int! - countProjects: Int! - countInvites: Int! - countFollows: Int! - countShouts: Int! -} - -type Notification { - id: ID! - read: Boolean, - user: User @relation(name: "NOTIFIED", direction: "OUT") - post: Post @relation(name: "NOTIFIED", direction: "IN") - createdAt: String -} - -scalar Date -scalar Time -scalar DateTime - -enum VisibilityEnum { - public - friends - private -} - -enum UserGroupEnum { - admin - moderator - user -} - -type Location { - id: ID! - name: String! - nameEN: String - nameDE: String - nameFR: String - nameNL: String - nameIT: String - nameES: String - namePT: String - namePL: String - type: String! - lat: Float - lng: Float - parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") -} - -type User { - id: ID! - actorId: String - name: String - email: String! - slug: String - password: String! - avatar: String - avatarUpload: Upload - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") - role: UserGroupEnum - publicKey: String - privateKey: String - - location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") - locationName: String - about: String - socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT") - - createdAt: String - updatedAt: String - - notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") - - friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") - friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") - - following: [User]! @relation(name: "FOLLOWS", direction: "OUT") - followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)") - - followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") - followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") - - # Is the currently logged in user following that user? - followedByCurrentUser: Boolean! @cypher( - statement: """ - MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 - """ - ) - - #contributions: [WrittenPost]! - #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! - # @cypher( - # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" - # ) - contributions: [Post]! @relation(name: "WROTE", direction: "OUT") - contributionsCount: Int! @cypher( - statement: """ - MATCH (this)-[:WROTE]->(r:Post) - WHERE (NOT exists(r.deleted) OR r.deleted = false) - AND (NOT exists(r.disabled) OR r.disabled = false) - RETURN COUNT(r) - """ - ) - - comments: [Comment]! @relation(name: "WROTE", direction: "OUT") - commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") - - shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") - shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") - - organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT") - organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT") - - blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT") - - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") - - badges: [Badge]! @relation(name: "REWARDED", direction: "IN") - badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") -} - -type Post { - id: ID! - activityId: String - objectId: String - author: User @relation(name: "WROTE", direction: "IN") - title: String! - slug: String - content: String! - contentExcerpt: String - image: String - imageUpload: Upload - visibility: VisibilityEnum - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") - createdAt: String - updatedAt: String - - relatedContributions: [Post]! @cypher( - statement: """ - MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) - RETURN DISTINCT post - LIMIT 10 - """ - ) - - tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") - - comments: [Comment]! @relation(name: "COMMENTS", direction: "IN") - commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") - - shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") - shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") - - # Has the currently logged in user shouted that post? - shoutedByCurrentUser: Boolean! @cypher( - statement: """ - MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 - """ - ) -} - -type Comment { - id: ID! - activityId: String - postId: ID - author: User @relation(name: "WROTE", direction: "IN") - content: String! - contentExcerpt: String - post: Post @relation(name: "COMMENTS", direction: "OUT") - createdAt: String - updatedAt: String - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") -} - -type Report { - id: ID! - submitter: User @relation(name: "REPORTED", direction: "IN") - description: String - type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") - createdAt: String - comment: Comment @relation(name: "REPORTED", direction: "OUT") - post: Post @relation(name: "REPORTED", direction: "OUT") - user: User @relation(name: "REPORTED", direction: "OUT") -} - -type Category { - id: ID! - name: String! - slug: String - icon: String! - posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN") - postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)") -} - -type Badge { - id: ID! - key: String! - type: BadgeTypeEnum! - status: BadgeStatusEnum! - icon: String! - - rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} - -enum BadgeTypeEnum { - role - crowdfunding -} -enum BadgeStatusEnum { - permanent - temporary -} -enum ShoutTypeEnum { - Post - Organization - Project -} -enum FollowTypeEnum { - User - Organization - Project -} - -type Reward { - id: ID! - user: User @relation(name: "REWARDED", direction: "IN") - rewarderId: ID - createdAt: String - badge: Badge @relation(name: "REWARDED", direction: "OUT") -} - -type Organization { - id: ID! - createdBy: User @relation(name: "CREATED_ORGA", direction: "IN") - ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN") - name: String! - slug: String - description: String! - descriptionExcerpt: String - deleted: Boolean - disabled: Boolean - - tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") -} - -type Tag { - id: ID! - name: String! - taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN") - taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN") - taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)") - taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)") - deleted: Boolean - disabled: Boolean -} - -type SharedInboxEndpoint { - id: ID! - uri: String -} - -type SocialMedia { - id: ID! - url: String - ownedBy: [User]! @relation(name: "OWNED", direction: "IN") -} - diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql index 68c5d5707..99015a518 100644 --- a/backend/src/schema/types/type/Badge.gql +++ b/backend/src/schema/types/type/Badge.gql @@ -1,6 +1,5 @@ type Badge { id: ID! - key: String! type: BadgeType! status: BadgeStatus! icon: String! @@ -10,4 +9,23 @@ type Badge { updatedAt: String rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} \ No newline at end of file +} + +enum BadgeStatus { + permanent + temporary +} + +enum BadgeType { + role + crowdfunding +} + +type Query { + Badge: [Badge] +} + +type Mutation { + reward(badgeKey: ID!, userId: ID!): User + unreward(badgeKey: ID!, userId: ID!): User +} diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql index 441fba179..4abebcba6 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/schema/types/type/Comment.gql @@ -24,7 +24,7 @@ type Mutation { ): Comment UpdateComment( id: ID! - content: String + content: String! contentExcerpt: String deleted: Boolean disabled: Boolean diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql new file mode 100644 index 000000000..80d655b5c --- /dev/null +++ b/backend/src/schema/types/type/EMOTED.gql @@ -0,0 +1,10 @@ +type EMOTED @relation(name: "EMOTED") { + from: User + to: Post + + emotion: Emotion + #createdAt: DateTime + #updatedAt: DateTime + createdAt: String + updatedAt: String +} \ No newline at end of file diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index deb1d8f85..d254a9a9c 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -48,6 +48,8 @@ type Post { RETURN COUNT(u) >= 1 """ ) + + emotions: [EMOTED] } type Mutation { diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 314f03521..81bf3e782 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -2,14 +2,14 @@ type User { id: ID! actorId: String name: String - email: String! + email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email") slug: String! avatar: String coverImg: String deleted: Boolean disabled: Boolean disabledBy: User @relation(name: "DISABLED", direction: "IN") - role: UserGroup + role: UserGroup! publicKey: String invitedBy: User @relation(name: "INVITED", direction: "IN") invited: [User] @relation(name: "INVITED", direction: "OUT") @@ -73,12 +73,17 @@ type User { badges: [Badge]! @relation(name: "REWARDED", direction: "IN") badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + + emotions: [EMOTED] } input _UserFilter { AND: [_UserFilter!] OR: [_UserFilter!] + name_contains: String + about_contains: String + slug_contains: String id: ID id_not: ID id_in: [ID!] diff --git a/backend/src/seed/factories/badges.js b/backend/src/seed/factories/badges.js index 6414e9f36..5f0482460 100644 --- a/backend/src/seed/factories/badges.js +++ b/backend/src/seed/factories/badges.js @@ -1,28 +1,15 @@ -import uuid from 'uuid/v4' - -export default function(params) { - const { - id = uuid(), - key = '', - type = 'crowdfunding', - status = 'permanent', - icon = '/img/badges/indiegogo_en_panda.svg', - } = params - +export default function create() { return { - mutation: ` - mutation( - $id: ID - $key: String! - $type: BadgeType! - $status: BadgeStatus! - $icon: String! - ) { - CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { - id - } + factory: async ({ args, neodeInstance }) => { + const defaults = { + type: 'crowdfunding', + status: 'permanent', } - `, - variables: { id, key, type, status, icon }, + args = { + ...defaults, + ...args, + } + return neodeInstance.create('Badge', args) + }, } } diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index b2cf2de45..e841b0beb 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -73,6 +73,7 @@ export default function Factory(options = {}) { const { factory, mutation, variables } = this.factories[node](args) if (factory) { this.lastResponse = await factory({ args, neodeInstance }) + return this.lastResponse } else { this.lastResponse = await this.graphQLClient.request(mutation, variables) } diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index ffe8e7a39..af1699253 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -3,7 +3,7 @@ import uuid from 'uuid/v4' import encryptPassword from '../../helpers/encryptPassword' import slugify from 'slug' -export default function create(params) { +export default function create() { return { factory: async ({ args, neodeInstance }) => { const defaults = { @@ -22,7 +22,10 @@ export default function create(params) { } args = await encryptPassword(args) const user = await neodeInstance.create('User', args) - return user.toJson() + const email = await neodeInstance.create('EmailAddress', { email: args.email }) + await user.relateTo(email, 'primaryEmail') + await email.relateTo(user, 'belongsTo') + return user }, } } diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index e31d09a68..8f693cfd3 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -5,52 +5,42 @@ import Factory from './factories' ;(async function() { try { const f = Factory() - await Promise.all([ + const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([ f.create('Badge', { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_racoon', icon: '/img/badges/indiegogo_en_racoon.svg', }), f.create('Badge', { - id: 'b2', - key: 'indiegogo_en_rabbit', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_rabbit', icon: '/img/badges/indiegogo_en_rabbit.svg', }), f.create('Badge', { - id: 'b3', - key: 'indiegogo_en_wolf', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_wolf', icon: '/img/badges/indiegogo_en_wolf.svg', }), f.create('Badge', { - id: 'b4', - key: 'indiegogo_en_bear', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_bear', icon: '/img/badges/indiegogo_en_bear.svg', }), f.create('Badge', { - id: 'b5', - key: 'indiegogo_en_turtle', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_turtle', icon: '/img/badges/indiegogo_en_turtle.svg', }), f.create('Badge', { - id: 'b6', - key: 'indiegogo_en_rhino', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_rhino', icon: '/img/badges/indiegogo_en_rhino.svg', }), ]) - await Promise.all([ + const [ + peterLustig, + bobDerBaumeister, + jennyRostock, + tick, // eslint-disable-line no-unused-vars + trick, // eslint-disable-line no-unused-vars + track, // eslint-disable-line no-unused-vars + dagobert, + ] = await Promise.all([ f.create('User', { id: 'u1', name: 'Peter Lustig', @@ -69,47 +59,130 @@ import Factory from './factories' role: 'user', email: 'user@example.org', }), - f.create('User', { id: 'u4', name: 'Tick', role: 'user', email: 'tick@example.org' }), - f.create('User', { id: 'u5', name: 'Trick', role: 'user', email: 'trick@example.org' }), - f.create('User', { id: 'u6', name: 'Track', role: 'user', email: 'track@example.org' }), - f.create('User', { id: 'u7', name: 'Dagobert', role: 'user', email: 'dagobert@example.org' }), + f.create('User', { + id: 'u4', + name: 'Tick', + role: 'user', + email: 'tick@example.org', + }), + f.create('User', { + id: 'u5', + name: 'Trick', + role: 'user', + email: 'trick@example.org', + }), + f.create('User', { + id: 'u6', + name: 'Track', + role: 'user', + email: 'track@example.org', + }), + f.create('User', { + id: 'u7', + name: 'Dagobert', + role: 'user', + email: 'dagobert@example.org', + }), ]) const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([ - Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'user@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'track@example.org', password: '1234' }), + Factory().authenticateAs({ + email: 'admin@example.org', + password: '1234', + }), + Factory().authenticateAs({ + email: 'moderator@example.org', + password: '1234', + }), + Factory().authenticateAs({ + email: 'user@example.org', + password: '1234', + }), + Factory().authenticateAs({ + email: 'tick@example.org', + password: '1234', + }), + Factory().authenticateAs({ + email: 'trick@example.org', + password: '1234', + }), + Factory().authenticateAs({ + email: 'track@example.org', + password: '1234', + }), ]) await Promise.all([ - f.relate('User', 'Badges', { from: 'b6', to: 'u1' }), - f.relate('User', 'Badges', { from: 'b5', to: 'u2' }), - f.relate('User', 'Badges', { from: 'b4', to: 'u3' }), - f.relate('User', 'Badges', { from: 'b3', to: 'u4' }), - f.relate('User', 'Badges', { from: 'b2', to: 'u5' }), - f.relate('User', 'Badges', { from: 'b1', to: 'u6' }), - f.relate('User', 'Friends', { from: 'u1', to: 'u2' }), - f.relate('User', 'Friends', { from: 'u1', to: 'u3' }), - f.relate('User', 'Friends', { from: 'u2', to: 'u3' }), - f.relate('User', 'Blacklisted', { from: 'u7', to: 'u4' }), - f.relate('User', 'Blacklisted', { from: 'u7', to: 'u5' }), - f.relate('User', 'Blacklisted', { from: 'u7', to: 'u6' }), + peterLustig.relateTo(racoon, 'rewarded'), + peterLustig.relateTo(rhino, 'rewarded'), + peterLustig.relateTo(wolf, 'rewarded'), + bobDerBaumeister.relateTo(racoon, 'rewarded'), + bobDerBaumeister.relateTo(turtle, 'rewarded'), + jennyRostock.relateTo(bear, 'rewarded'), + dagobert.relateTo(rabbit, 'rewarded'), ]) await Promise.all([ - asAdmin.follow({ id: 'u3', type: 'User' }), - asModerator.follow({ id: 'u4', type: 'User' }), - asUser.follow({ id: 'u4', type: 'User' }), - asTick.follow({ id: 'u6', type: 'User' }), - asTrick.follow({ id: 'u4', type: 'User' }), - asTrack.follow({ id: 'u3', type: 'User' }), + f.relate('User', 'Friends', { + from: 'u1', + to: 'u2', + }), + f.relate('User', 'Friends', { + from: 'u1', + to: 'u3', + }), + f.relate('User', 'Friends', { + from: 'u2', + to: 'u3', + }), + f.relate('User', 'Blacklisted', { + from: 'u7', + to: 'u4', + }), + f.relate('User', 'Blacklisted', { + from: 'u7', + to: 'u5', + }), + f.relate('User', 'Blacklisted', { + from: 'u7', + to: 'u6', + }), ]) await Promise.all([ - f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }), + asAdmin.follow({ + id: 'u3', + type: 'User', + }), + asModerator.follow({ + id: 'u4', + type: 'User', + }), + asUser.follow({ + id: 'u4', + type: 'User', + }), + asTick.follow({ + id: 'u6', + type: 'User', + }), + asTrick.follow({ + id: 'u4', + type: 'User', + }), + asTrack.follow({ + id: 'u3', + type: 'User', + }), + ]) + + await Promise.all([ + f.create('Category', { + id: 'cat1', + name: 'Just For Fun', + slug: 'justforfun', + icon: 'smile', + }), f.create('Category', { id: 'cat2', name: 'Happyness & Values', @@ -203,10 +276,22 @@ import Factory from './factories' ]) await Promise.all([ - f.create('Tag', { id: 't1', name: 'Umwelt' }), - f.create('Tag', { id: 't2', name: 'Naturschutz' }), - f.create('Tag', { id: 't3', name: 'Demokratie' }), - f.create('Tag', { id: 't4', name: 'Freiheit' }), + f.create('Tag', { + id: 'Umwelt', + name: 'Umwelt', + }), + f.create('Tag', { + id: 'Naturschutz', + name: 'Naturschutz', + }), + f.create('Tag', { + id: 'Demokratie', + name: 'Demokratie', + }), + f.create('Tag', { + id: 'Freiheit', + name: 'Freiheit', + }), ]) const mention1 = 'Hey @jenny-rostock, what\'s up?' @@ -214,108 +299,347 @@ import Factory from './factories' 'Hey @jenny-rostock, here is another notification for you!' await Promise.all([ - asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food() }), - asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology() }), - asUser.create('Post', { id: 'p2' }), - asTick.create('Post', { id: 'p3' }), - asTrick.create('Post', { id: 'p4' }), - asTrack.create('Post', { id: 'p5' }), - asAdmin.create('Post', { id: 'p6', image: faker.image.unsplash.buildings() }), - asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }), - asUser.create('Post', { id: 'p8', image: faker.image.unsplash.nature() }), - asTick.create('Post', { id: 'p9' }), - asTrick.create('Post', { id: 'p10' }), - asTrack.create('Post', { id: 'p11', image: faker.image.unsplash.people() }), - asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }), - asModerator.create('Post', { id: 'p13' }), - asUser.create('Post', { id: 'p14', image: faker.image.unsplash.objects() }), - asTick.create('Post', { id: 'p15' }), + asAdmin.create('Post', { + id: 'p0', + image: faker.image.unsplash.food(), + }), + asModerator.create('Post', { + id: 'p1', + image: faker.image.unsplash.technology(), + }), + asUser.create('Post', { + id: 'p2', + }), + asTick.create('Post', { + id: 'p3', + }), + asTrick.create('Post', { + id: 'p4', + }), + asTrack.create('Post', { + id: 'p5', + }), + asAdmin.create('Post', { + id: 'p6', + image: faker.image.unsplash.buildings(), + }), + asModerator.create('Post', { + id: 'p7', + content: `${mention1} ${faker.lorem.paragraph()}`, + }), + asUser.create('Post', { + id: 'p8', + image: faker.image.unsplash.nature(), + }), + asTick.create('Post', { + id: 'p9', + }), + asTrick.create('Post', { + id: 'p10', + }), + asTrack.create('Post', { + id: 'p11', + image: faker.image.unsplash.people(), + }), + asAdmin.create('Post', { + id: 'p12', + content: `${mention2} ${faker.lorem.paragraph()}`, + }), + asModerator.create('Post', { + id: 'p13', + }), + asUser.create('Post', { + id: 'p14', + image: faker.image.unsplash.objects(), + }), + asTick.create('Post', { + id: 'p15', + }), ]) await Promise.all([ - f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), - f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }), - f.relate('Post', 'Categories', { from: 'p2', to: 'cat2' }), - f.relate('Post', 'Categories', { from: 'p3', to: 'cat3' }), - f.relate('Post', 'Categories', { from: 'p4', to: 'cat4' }), - f.relate('Post', 'Categories', { from: 'p5', to: 'cat5' }), - f.relate('Post', 'Categories', { from: 'p6', to: 'cat6' }), - f.relate('Post', 'Categories', { from: 'p7', to: 'cat7' }), - f.relate('Post', 'Categories', { from: 'p8', to: 'cat8' }), - f.relate('Post', 'Categories', { from: 'p9', to: 'cat9' }), - f.relate('Post', 'Categories', { from: 'p10', to: 'cat10' }), - f.relate('Post', 'Categories', { from: 'p11', to: 'cat11' }), - f.relate('Post', 'Categories', { from: 'p12', to: 'cat12' }), - f.relate('Post', 'Categories', { from: 'p13', to: 'cat13' }), - f.relate('Post', 'Categories', { from: 'p14', to: 'cat14' }), - f.relate('Post', 'Categories', { from: 'p15', to: 'cat15' }), + f.relate('Post', 'Categories', { + from: 'p0', + to: 'cat16', + }), + f.relate('Post', 'Categories', { + from: 'p1', + to: 'cat1', + }), + f.relate('Post', 'Categories', { + from: 'p2', + to: 'cat2', + }), + f.relate('Post', 'Categories', { + from: 'p3', + to: 'cat3', + }), + f.relate('Post', 'Categories', { + from: 'p4', + to: 'cat4', + }), + f.relate('Post', 'Categories', { + from: 'p5', + to: 'cat5', + }), + f.relate('Post', 'Categories', { + from: 'p6', + to: 'cat6', + }), + f.relate('Post', 'Categories', { + from: 'p7', + to: 'cat7', + }), + f.relate('Post', 'Categories', { + from: 'p8', + to: 'cat8', + }), + f.relate('Post', 'Categories', { + from: 'p9', + to: 'cat9', + }), + f.relate('Post', 'Categories', { + from: 'p10', + to: 'cat10', + }), + f.relate('Post', 'Categories', { + from: 'p11', + to: 'cat11', + }), + f.relate('Post', 'Categories', { + from: 'p12', + to: 'cat12', + }), + f.relate('Post', 'Categories', { + from: 'p13', + to: 'cat13', + }), + f.relate('Post', 'Categories', { + from: 'p14', + to: 'cat14', + }), + f.relate('Post', 'Categories', { + from: 'p15', + to: 'cat15', + }), - f.relate('Post', 'Tags', { from: 'p0', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p1', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p2', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p3', to: 't3' }), - f.relate('Post', 'Tags', { from: 'p4', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p5', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p6', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p7', to: 't3' }), - f.relate('Post', 'Tags', { from: 'p8', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p9', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p10', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p11', to: 't3' }), - f.relate('Post', 'Tags', { from: 'p12', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p13', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p14', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p15', to: 't3' }), + f.relate('Post', 'Tags', { + from: 'p0', + to: 'Freiheit', + }), + f.relate('Post', 'Tags', { + from: 'p1', + to: 'Umwelt', + }), + f.relate('Post', 'Tags', { + from: 'p2', + to: 'Naturschutz', + }), + f.relate('Post', 'Tags', { + from: 'p3', + to: 'Demokratie', + }), + f.relate('Post', 'Tags', { + from: 'p4', + to: 'Freiheit', + }), + f.relate('Post', 'Tags', { + from: 'p5', + to: 'Umwelt', + }), + f.relate('Post', 'Tags', { + from: 'p6', + to: 'Naturschutz', + }), + f.relate('Post', 'Tags', { + from: 'p7', + to: 'Demokratie', + }), + f.relate('Post', 'Tags', { + from: 'p8', + to: 'Freiheit', + }), + f.relate('Post', 'Tags', { + from: 'p9', + to: 'Umwelt', + }), + f.relate('Post', 'Tags', { + from: 'p10', + to: 'Naturschutz', + }), + f.relate('Post', 'Tags', { + from: 'p11', + to: 'Demokratie', + }), + f.relate('Post', 'Tags', { + from: 'p12', + to: 'Freiheit', + }), + f.relate('Post', 'Tags', { + from: 'p13', + to: 'Umwelt', + }), + f.relate('Post', 'Tags', { + from: 'p14', + to: 'Naturschutz', + }), + f.relate('Post', 'Tags', { + from: 'p15', + to: 'Demokratie', + }), ]) await Promise.all([ - asAdmin.shout({ id: 'p2', type: 'Post' }), - asAdmin.shout({ id: 'p6', type: 'Post' }), - asModerator.shout({ id: 'p0', type: 'Post' }), - asModerator.shout({ id: 'p6', type: 'Post' }), - asUser.shout({ id: 'p6', type: 'Post' }), - asUser.shout({ id: 'p7', type: 'Post' }), - asTick.shout({ id: 'p8', type: 'Post' }), - asTick.shout({ id: 'p9', type: 'Post' }), - asTrack.shout({ id: 'p10', type: 'Post' }), + asAdmin.shout({ + id: 'p2', + type: 'Post', + }), + asAdmin.shout({ + id: 'p6', + type: 'Post', + }), + asModerator.shout({ + id: 'p0', + type: 'Post', + }), + asModerator.shout({ + id: 'p6', + type: 'Post', + }), + asUser.shout({ + id: 'p6', + type: 'Post', + }), + asUser.shout({ + id: 'p7', + type: 'Post', + }), + asTick.shout({ + id: 'p8', + type: 'Post', + }), + asTick.shout({ + id: 'p9', + type: 'Post', + }), + asTrack.shout({ + id: 'p10', + type: 'Post', + }), ]) await Promise.all([ - asAdmin.shout({ id: 'p2', type: 'Post' }), - asAdmin.shout({ id: 'p6', type: 'Post' }), - asModerator.shout({ id: 'p0', type: 'Post' }), - asModerator.shout({ id: 'p6', type: 'Post' }), - asUser.shout({ id: 'p6', type: 'Post' }), - asUser.shout({ id: 'p7', type: 'Post' }), - asTick.shout({ id: 'p8', type: 'Post' }), - asTick.shout({ id: 'p9', type: 'Post' }), - asTrack.shout({ id: 'p10', type: 'Post' }), + asAdmin.shout({ + id: 'p2', + type: 'Post', + }), + asAdmin.shout({ + id: 'p6', + type: 'Post', + }), + asModerator.shout({ + id: 'p0', + type: 'Post', + }), + asModerator.shout({ + id: 'p6', + type: 'Post', + }), + asUser.shout({ + id: 'p6', + type: 'Post', + }), + asUser.shout({ + id: 'p7', + type: 'Post', + }), + asTick.shout({ + id: 'p8', + type: 'Post', + }), + asTick.shout({ + id: 'p9', + type: 'Post', + }), + asTrack.shout({ + id: 'p10', + type: 'Post', + }), ]) await Promise.all([ - asUser.create('Comment', { id: 'c1', postId: 'p1' }), - asTick.create('Comment', { id: 'c2', postId: 'p1' }), - asTrack.create('Comment', { id: 'c3', postId: 'p3' }), - asTrick.create('Comment', { id: 'c4', postId: 'p2' }), - asModerator.create('Comment', { id: 'c5', postId: 'p3' }), - asAdmin.create('Comment', { id: 'c6', postId: 'p4' }), - asUser.create('Comment', { id: 'c7', postId: 'p2' }), - asTick.create('Comment', { id: 'c8', postId: 'p15' }), - asTrick.create('Comment', { id: 'c9', postId: 'p15' }), - asTrack.create('Comment', { id: 'c10', postId: 'p15' }), - asUser.create('Comment', { id: 'c11', postId: 'p15' }), - asUser.create('Comment', { id: 'c12', postId: 'p15' }), + asUser.create('Comment', { + id: 'c1', + postId: 'p1', + }), + asTick.create('Comment', { + id: 'c2', + postId: 'p1', + }), + asTrack.create('Comment', { + id: 'c3', + postId: 'p3', + }), + asTrick.create('Comment', { + id: 'c4', + postId: 'p2', + }), + asModerator.create('Comment', { + id: 'c5', + postId: 'p3', + }), + asAdmin.create('Comment', { + id: 'c6', + postId: 'p4', + }), + asUser.create('Comment', { + id: 'c7', + postId: 'p2', + }), + asTick.create('Comment', { + id: 'c8', + postId: 'p15', + }), + asTrick.create('Comment', { + id: 'c9', + postId: 'p15', + }), + asTrack.create('Comment', { + id: 'c10', + postId: 'p15', + }), + asUser.create('Comment', { + id: 'c11', + postId: 'p15', + }), + asUser.create('Comment', { + id: 'c12', + postId: 'p15', + }), ]) const disableMutation = 'mutation($id: ID!) { disable(id: $id) }' await Promise.all([ - asModerator.mutate(disableMutation, { id: 'p11' }), - asModerator.mutate(disableMutation, { id: 'c5' }), + asModerator.mutate(disableMutation, { + id: 'p11', + }), + asModerator.mutate(disableMutation, { + id: 'c5', + }), ]) await Promise.all([ - asTick.create('Report', { description: "I don't like this comment", id: 'c1' }), - asTrick.create('Report', { description: "I don't like this post", id: 'p1' }), - asTrack.create('Report', { description: "I don't like this user", id: 'u1' }), + asTick.create('Report', { + description: "I don't like this comment", + id: 'c1', + }), + asTrick.create('Report', { + description: "I don't like this post", + id: 'p1', + }), + asTrack.create('Report', { + description: "I don't like this user", + id: 'u1', + }), ]) await Promise.all([ @@ -342,11 +666,30 @@ import Factory from './factories' ]) await Promise.all([ - f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o1' }), - f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o2' }), - f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o2' }), - f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o3' }), + f.relate('Organization', 'CreatedBy', { + from: 'u1', + to: 'o1', + }), + f.relate('Organization', 'CreatedBy', { + from: 'u1', + to: 'o2', + }), + f.relate('Organization', 'OwnedBy', { + from: 'u2', + to: 'o2', + }), + f.relate('Organization', 'OwnedBy', { + from: 'u2', + to: 'o3', + }), ]) + + await Promise.all( + [...Array(30).keys()].map(i => { + return f.create('User') + }), + ) + /* eslint-disable-next-line no-console */ console.log('Seeded Data...') process.exit(0) diff --git a/backend/src/server.js b/backend/src/server.js index 7692f0d2c..d58ecd277 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,6 +1,6 @@ import express from 'express' import helmet from 'helmet' -import { GraphQLServer } from 'graphql-yoga' +import { ApolloServer } from 'apollo-server-express' import CONFIG, { requiredConfigs } from './config' import mocks from './mocks' import middleware from './middleware' @@ -20,28 +20,30 @@ const driver = getDriver() const createServer = options => { const defaults = { - context: async ({ request }) => { - const user = await decode(driver, request.headers.authorization) + context: async ({ req }) => { + const user = await decode(driver, req.headers.authorization) return { driver, user, - req: request, + req, cypherParams: { currentUserId: user ? user.id : null, }, } }, - schema, + schema: middleware(schema), debug: CONFIG.DEBUG, tracing: CONFIG.DEBUG, - middlewares: middleware(schema), mocks: CONFIG.MOCKS ? mocks : false, } - const server = new GraphQLServer(Object.assign({}, defaults, options)) + const server = new ApolloServer(Object.assign({}, defaults, options)) - server.express.use(helmet()) - server.express.use(express.static('public')) - return server + const app = express() + app.use(helmet()) + app.use(express.static('public')) + server.applyMiddleware({ app, path: '/' }) + + return { server, app } } export default createServer diff --git a/backend/src/server.spec.js b/backend/src/server.spec.js new file mode 100644 index 000000000..6d4ef546d --- /dev/null +++ b/backend/src/server.spec.js @@ -0,0 +1,43 @@ +import { createTestClient } from 'apollo-server-testing' +import createServer from './server' + +/** + * This file is for demonstration purposes. It does not really test the + * `isLoggedIn` query but demonstrates how we can use `apollo-server-testing`. + * All we need to do is to get an instance of `ApolloServer` and maybe we want + * stub out `context` as shown below. + * + */ + +let user +let action +describe('isLoggedIn', () => { + beforeEach(() => { + action = async () => { + const { server } = createServer({ + context: () => { + return { + user, + } + }, + }) + const { query } = createTestClient(server) + + const isLoggedIn = `{ isLoggedIn }` + return query({ query: isLoggedIn }) + } + }) + + it('returns false', async () => { + const expected = expect.objectContaining({ data: { isLoggedIn: false } }) + await expect(action()).resolves.toEqual(expected) + }) + + describe('when authenticated', () => { + it('returns true', async () => { + user = { id: '123' } + const expected = expect.objectContaining({ data: { isLoggedIn: true } }) + await expect(action()).resolves.toEqual(expected) + }) + }) +}) diff --git a/backend/yarn.lock b/backend/yarn.lock index 596a0c917..d8f6991d2 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -9,10 +9,10 @@ dependencies: apollo-env "0.5.1" -"@apollographql/graphql-playground-html@1.6.20": - version "1.6.20" - resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d" - integrity sha512-3LWZa80HcP70Pl+H4KhLDJ7S0px+9/c8GTXdl6SpunRecUaB27g/oOQnAjNHLHdbWuGE0uyqcuGiTfbKB3ilaQ== +"@apollographql/graphql-playground-html@1.6.24": + version "1.6.24" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" + integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== "@babel/cli@~7.5.0": version "7.5.0" @@ -38,14 +38,14 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.0.tgz#6ed6a2881ad48a732c5433096d96d1b0ee5eb734" - integrity sha512-6Isr4X98pwXqHvtigw71CKgmhL1etZjPs5A67jL/w0TkLM9eqmFR40YrnJvEc1WnMZFsskjsmid8bHZyxKEAnw== +"@babel/core@^7.1.0", "@babel/core@~7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd" + integrity sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ== dependencies: "@babel/code-frame" "^7.0.0" "@babel/generator" "^7.5.0" - "@babel/helpers" "^7.5.0" + "@babel/helpers" "^7.5.4" "@babel/parser" "^7.5.0" "@babel/template" "^7.4.4" "@babel/traverse" "^7.5.0" @@ -260,10 +260,10 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.5.0": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.2.tgz#97424dc82fc0041f4c751119b4d2b1ec68cdb5ba" - integrity sha512-NDkkTqDvgFUeo8djXBOiwO/mFjownznOWvmP9hvNdfiFUmx0nwNOqxuaTTbxjH744eQsD9M5ubC7gdANBvIWPw== +"@babel/helpers@^7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.4.tgz#2f00608aa10d460bde0ccf665d6dcf8477357cf0" + integrity sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow== dependencies: "@babel/template" "^7.4.4" "@babel/traverse" "^7.5.0" @@ -320,10 +320,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.5.2": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.2.tgz#ec92b0c6419074ea7af77c78b7c5d42041f2f5a9" - integrity sha512-C/JU3YOx5J4d9s0GGlJlYXVwsbd5JmqQ0AvB7cIDAx7nN57aDTnlJEsZJPuSskeBtMGFWSWU5Q+piTiDe0s7FQ== +"@babel/plugin-proposal-object-rest-spread@^7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz#250de35d867ce8260a31b1fdac6c4fc1baa99331" + integrity sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -649,17 +649,17 @@ core-js "^2.5.7" regenerator-runtime "^0.12.0" -"@babel/preset-env@~7.5.2": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.2.tgz#34a46f01aed617b174b8dbaf8fed9239300343d0" - integrity sha512-7rRJLaUqJhQ+8xGrWtMROAgOi/+udIzyK2ES9NHhDIUvR2zfx/ON5lRR8ACUGehIYst8KVbl4vpkgOqn08gBxA== +"@babel/preset-env@~7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d" + integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-dynamic-import" "^7.5.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.5.2" + "@babel/plugin-proposal-object-rest-spread" "^7.5.4" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" "@babel/plugin-syntax-async-generators" "^7.2.0" @@ -787,7 +787,7 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.0.1.tgz#9712fa2ad124ac64668ab06ba847b1eaf83a03fd" integrity sha512-cctMYH5RLbElaUpZn3IJaUj9QNQD8iXDnl7xNY6KB1aFD2ciJrwpo3kvZowIT75uA+silJFDnSR2kGakALUymg== -"@hapi/joi@^15.1.0": +"@hapi/joi@^15.0.3", "@hapi/joi@^15.1.0": version "15.1.0" resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.0.tgz#940cb749b5c55c26ab3b34ce362e82b6162c8e7a" integrity sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ== @@ -1084,7 +1084,7 @@ "@types/node" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@4.17.0", "@types/express@^4.11.1": +"@types/express@*", "@types/express@4.17.0": version "4.17.0" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287" integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw== @@ -1093,16 +1093,6 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" -"@types/graphql-deduplicator@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/graphql-deduplicator/-/graphql-deduplicator-2.0.0.tgz#9e577b8f3feb3d067b0ca756f4a1fb356d533922" - integrity sha512-swUwj5hWF1yFzbUXStLJrUa0ksAt11B8+SwhsAjQAX0LYJ1LLioAyuDcJ9bovWbsNzIXJYXLvljSPQw8nR728w== - -"@types/graphql@^14.0.0": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c" - integrity sha512-TcFkpEjcQK7w8OcrQcd7iIBPjU0rdyi3ldj6d0iJ4PPSzbWqPBvXj9KSwO14hTOX2dm9RoiH7VuxksJLNYdXUQ== - "@types/istanbul-lib-coverage@*": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1179,11 +1169,6 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.21.tgz#bfca27a02a0631495bfd25b6c63647a125e6944e" integrity sha512-1C45M7hZrVsl8bXxYV01bitRp0r35djt+eX5HWFH3NdH+8ejqta3KM7rmQLRLrupkhF7jRkAtXl2EgDsriIqwA== -"@types/zen-observable@^0.5.3": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.4.tgz#b863a4191e525206819e008097ebf0fb2e3a1cdc" - integrity sha512-sW6xN96wUak4tgc89d0tbTg7QDGYhGv5hvQIS6h4mRCd8h2btiZ80loPU8cyLwsBbA4ZeQt0FjvUhJ4rNhdsGg== - "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -1343,20 +1328,21 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.4.tgz#0cb5c7be0e0dd0c44b1257144cd7f9f2a3c374e6" - integrity sha512-TVACHwcEF4wfHo5H9FLnoNjo0SLDo2jPW+bXs9aw0Y4Z2UisskSAPnIYOqUPnU8SoeNvs7zWgbLizq11SRTJtg== +apollo-cache-control@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.5.tgz#5d8b949bd9b4f03ca32c7d7e429f509c6881eefc" + integrity sha512-zCPwHjbo/VlmXl0sclZfBq/MlVVeGUAg02Q259OIXSgHBvn9BbExyz+EkO/DJvZfGMquxqS1X1BFO3VKuLUTdw== dependencies: apollo-server-env "2.4.0" - graphql-extensions "0.7.4" + graphql-extensions "0.7.7" -apollo-cache-control@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" - integrity sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA== +apollo-cache-control@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.0.tgz#08b157e5f8cd86f63608b05d45222de0725ebd5a" + integrity sha512-BBnfUmSWRws5dRSDD+R56RLJCE9v6xQuob+i/1Ju9EX4LZszU5JKVmxEvnkJ1bk/BkihjoQXTnP6fJCnt6fCmA== dependencies: - graphql-extensions "^0.0.x" + apollo-server-env "2.4.0" + graphql-extensions "0.8.0" apollo-cache-inmemory@~1.6.2: version "1.6.2" @@ -1399,6 +1385,14 @@ apollo-datasource@0.5.0: apollo-server-caching "0.4.0" apollo-server-env "2.4.0" +apollo-datasource@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.0.tgz#823d6be8a3804613b5c56d2972c07db662293fc6" + integrity sha512-DOzzYWEOReYRu2vWPKEulqlTb9Xjg67sjVCzve5MXa7GUXjfr8IKioljvfoBMlqm/PpbJVk2ci4n5NIFqoYsrQ== + dependencies: + apollo-server-caching "0.5.0" + apollo-server-env "2.4.0" + apollo-engine-reporting-protobuf@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.3.1.tgz#a581257fa8e3bb115ce38bf1b22e052d1475ad69" @@ -1406,29 +1400,36 @@ apollo-engine-reporting-protobuf@0.3.1: dependencies: protobufjs "^6.8.6" -apollo-engine-reporting@1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.4.tgz#65e12f94221d80b3b1740c26e82ce9bb6bdfb7ee" - integrity sha512-DJdYghyUBzT0/LcPLwuQNXDCw06r1RfxkVfNTGKoTv6a+leVvjhDJmXvc+jSuBPwaNsc+RYRnfyQ2qUn9fmfyA== +apollo-engine-reporting-protobuf@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec" + integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA== dependencies: - apollo-engine-reporting-protobuf "0.3.1" - apollo-graphql "^0.3.2" - apollo-server-core "2.6.6" - apollo-server-env "2.4.0" - async-retry "^1.2.1" - graphql-extensions "0.7.5" + protobufjs "^6.8.6" -apollo-engine-reporting@1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.5.tgz#075424d39dfe77a20f96e8e33b7ae52d58c38e1e" - integrity sha512-pSwjPgXK/elFsR22LXALtT3jI4fpEpeTNTHgNwLVLohaolusMYgBc/9FnVyFWFfMFS9k+3RmfeQdHhZ6T7WKFQ== +apollo-engine-reporting@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.6.tgz#579ba2da85ff848bd92be1b0f1ad61f0c57e3585" + integrity sha512-oCoFAUBGveg1i1Sao/2gNsf1kirJBT6vw6Zan9BCNUkyh68ewDts+xRg32VnD9lDhaHpXVJ3tVtuaV44HmdSEw== dependencies: apollo-engine-reporting-protobuf "0.3.1" apollo-graphql "^0.3.3" - apollo-server-core "2.6.7" + apollo-server-core "2.6.9" apollo-server-env "2.4.0" async-retry "^1.2.1" - graphql-extensions "0.7.6" + graphql-extensions "0.7.7" + +apollo-engine-reporting@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.0.tgz#3a9bd011b271593e16d7057044898d0a817b197d" + integrity sha512-NMiO3h1cuEBt6QZNGHxivwuyZQnoU/2MMx0gUA8Gyy1ERBhK6P235qoMnvoi34rLmqJuyGPX6tXcab8MpMIzYQ== + dependencies: + apollo-engine-reporting-protobuf "0.4.0" + apollo-graphql "^0.3.3" + apollo-server-env "2.4.0" + apollo-server-types "0.2.0" + async-retry "^1.2.1" + graphql-extensions "0.8.0" apollo-env@0.5.1: version "0.5.1" @@ -1447,14 +1448,6 @@ apollo-errors@^1.9.0: assert "^1.4.1" extendable-error "^0.1.5" -apollo-graphql@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a" - integrity sha512-YbzYGR14GV0023m//EU66vOzZ3i7c04V/SF8Qk+60vf1sOWyKgO6mxZJ4BKhw10qWUayirhSDxq3frYE+qSG0A== - dependencies: - apollo-env "0.5.1" - lodash.sortby "^4.7.0" - apollo-graphql@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.3.tgz#ce1df194f6e547ad3ce1e35b42f9c211766e1658" @@ -1506,24 +1499,31 @@ apollo-server-caching@0.4.0: dependencies: lru-cache "^5.0.0" -apollo-server-core@2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.6.tgz#55fea7980a943948c49dea20d81b9bbfc0e04f87" - integrity sha512-PFSjJbqkV1eetfFJxu11gzklQYC8BrF0RZfvC1d1mhvtxAOKl25uhPHxltN0Omyjp7LW4YeoC6zwl9rLWuhZFQ== +apollo-server-caching@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46" + integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw== + dependencies: + lru-cache "^5.0.0" + +apollo-server-core@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.9.tgz#75542ad206782e5c31a023b54962e9fdc6404a91" + integrity sha512-r2/Kjm1UmxoTViUt5EcExWXkWl0riXsuGyS1q5LpHKKnA+6b+t4LQKECkRU4EWNpuuzJQn7aF7MmMdvURxoEig== dependencies: "@apollographql/apollo-tools" "^0.3.6" - "@apollographql/graphql-playground-html" "1.6.20" + "@apollographql/graphql-playground-html" "1.6.24" "@types/ws" "^6.0.0" - apollo-cache-control "0.7.4" + apollo-cache-control "0.7.5" apollo-datasource "0.5.0" - apollo-engine-reporting "1.3.4" + apollo-engine-reporting "1.3.6" apollo-server-caching "0.4.0" apollo-server-env "2.4.0" - apollo-server-errors "2.3.0" - apollo-server-plugin-base "0.5.5" - apollo-tracing "0.7.3" + apollo-server-errors "2.3.1" + apollo-server-plugin-base "0.5.8" + apollo-tracing "0.7.4" fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.7.5" + graphql-extensions "0.7.7" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" @@ -1532,24 +1532,26 @@ apollo-server-core@2.6.6: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" -apollo-server-core@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.7.tgz#85b0310f40cfec43a702569c73af16d88776a6f0" - integrity sha512-HfOGLvEwPgDWTvd3ZKRPEkEnICKb7xadn1Mci4+auMTsL/NVkfpjPa8cdzubi/kS2/MvioIn7Bg74gmiSLghGQ== +apollo-server-core@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.7.0.tgz#c444347dea11149b5b453890506e43dc7e711257" + integrity sha512-CXjXAkgcMBCJZpsZgfAY5W7f5thdxUhn75UgzeH28RTUZ2aKi/LjoCixPWRSF1lU4vuEWneAnM8Vg/KCD+29lQ== dependencies: "@apollographql/apollo-tools" "^0.3.6" - "@apollographql/graphql-playground-html" "1.6.20" + "@apollographql/graphql-playground-html" "1.6.24" "@types/ws" "^6.0.0" - apollo-cache-control "0.7.4" - apollo-datasource "0.5.0" - apollo-engine-reporting "1.3.5" - apollo-server-caching "0.4.0" + apollo-cache-control "0.8.0" + apollo-datasource "0.6.0" + apollo-engine-reporting "1.4.0" + apollo-engine-reporting-protobuf "0.4.0" + apollo-server-caching "0.5.0" apollo-server-env "2.4.0" - apollo-server-errors "2.3.0" - apollo-server-plugin-base "0.5.6" - apollo-tracing "0.7.3" + apollo-server-errors "2.3.1" + apollo-server-plugin-base "0.6.0" + apollo-server-types "0.2.0" + apollo-tracing "0.8.0" fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.7.6" + graphql-extensions "0.8.0" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" @@ -1558,15 +1560,6 @@ apollo-server-core@2.6.7: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" -apollo-server-core@^1.3.6, apollo-server-core@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592" - integrity sha512-BP1Vh39krgEjkQxbjTdBURUjLHbFq1zeOChDJgaRsMxGtlhzuLWwwC6lLdPatN8jEPbeHq8Tndp9QZ3iQZOKKA== - dependencies: - apollo-cache-control "^0.1.0" - apollo-tracing "^0.1.0" - graphql-extensions "^0.0.x" - apollo-server-env@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872" @@ -1575,102 +1568,83 @@ apollo-server-env@2.4.0: node-fetch "^2.1.2" util.promisify "^1.0.0" -apollo-server-errors@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061" - integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw== +apollo-server-errors@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz#033cf331463ebb99a563f8354180b41ac6714eb6" + integrity sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg== -apollo-server-express@2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.6.tgz#ec2b955354d7dd4d12fe01ea7e983d302071d5b9" - integrity sha512-bY/xrr9lZH+hsjchiQuSXpW3ivXfL1h81M5VE9Ppus1PVwwEIar/irBN+PFp97WxERZPDjVZzrRKa+lRHjtJsA== +apollo-server-express@2.6.9, apollo-server-express@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.9.tgz#176dab7f2cd5a99655c8eb382ad9b10797422a7b" + integrity sha512-iTkdIdX7m9EAlmL/ZPkKR+x/xuFk1HYZWuJIJG57hHUhcOxj50u7F1E5+5fDwl5RFIdepQ61azF31hhNZuNi4g== dependencies: - "@apollographql/graphql-playground-html" "1.6.20" + "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" "@types/body-parser" "1.17.0" "@types/cors" "^2.8.4" "@types/express" "4.17.0" accepts "^1.3.5" - apollo-server-core "2.6.6" + apollo-server-core "2.6.9" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" type-is "^1.6.16" -apollo-server-express@^1.3.6: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-1.4.0.tgz#7d7c58d6d6f9892b83fe575669093bb66738b125" - integrity sha512-zkH00nxhLnJfO0HgnNPBTfZw8qI5ILaPZ5TecMCI9+Y9Ssr2b0bFr9pBRsXy9eudPhI+/O4yqegSUsnLdF/CPw== +apollo-server-plugin-base@0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.8.tgz#77b4127aff4e3514a9d49e3cc61256aee4d9422e" + integrity sha512-ICbaXr0ycQZL5llbtZhg8zyHbxuZ4khdAJsJgiZaUXXP6+F47XfDQ5uwnl/4Sq9fvkpwS0ctvfZ1D+Ks4NvUzA== + +apollo-server-plugin-base@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.0.tgz#4186296ea5d52cfe613961d252a8a2f9e13e6ba6" + integrity sha512-BjfyWpHyKwHOe819gk3wEFwbnVp9Xvos03lkkYTTcXS/8G7xO78aUcE65mmyAC56/ZQ0aodNFkFrhwNtWBQWUQ== dependencies: - apollo-server-core "^1.4.0" - apollo-server-module-graphiql "^1.4.0" + apollo-server-types "0.2.0" -apollo-server-lambda@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/apollo-server-lambda/-/apollo-server-lambda-1.3.6.tgz#bdaac37f143c6798e40b8ae75580ba673cea260e" - integrity sha1-varDfxQ8Z5jkC4rnVYC6ZzzqJg4= +apollo-server-testing@~2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.7.0.tgz#df8f8fb0df85f9781b6822fc92ee36fdc039b024" + integrity sha512-geBTK5T8mqZ2UOscC3oHQ/QoKMINLK+Mmq5ZfZe9UhmdXi+WqQ3/6B+3HMoQ7eQ7D6bNILe8b7N2EKuyUkePdg== dependencies: - apollo-server-core "^1.3.6" - apollo-server-module-graphiql "^1.3.4" + apollo-server-core "2.7.0" -apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec" - integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA== - -apollo-server-plugin-base@0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.5.tgz#364e4a2fca4d95ddeb9fd3e78940ed1da58865c2" - integrity sha512-agiuhknyu3lnnEsqUh99tzxwPCGp+TuDK+TSRTkXU1RUG6lY4C3uJp0JGJw03cP+M6ze73TbRjMA4E68g/ks5A== - -apollo-server-plugin-base@0.5.6: - version "0.5.6" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.6.tgz#3a7128437a0f845e7d873fa43ef091ff7bf27975" - integrity sha512-wJvcPqfm/kiBwY5JZT85t2A4pcHv24xdQIpWMNt1zsnx77lIZqJmhsc22eSUSrlnYqUMXC4XMVgSUfAO4oI9wg== - -apollo-server-testing@~2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.7.tgz#cfc6366921eb99fd0cbc5d02552a8a5b268787d5" - integrity sha512-lqgZuSqBd5hkRILeVEleo2ScJjukR/E71Mv67vPBUs01s0gEHNnjSRnuOJJOM3cAFBQOdKPc42cHGANzf2ZZTw== +apollo-server-types@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.0.tgz#270d7298f709fd8237ebfa48753249e5286df5f2" + integrity sha512-5dgiyXsM90vnfmdXO1ixHvsLn0d9NP4tWufmr3ZmjKv00r4JAQNUaUdgOSGbRIKoHELQGwxUuTySTZ/tYfGaNQ== dependencies: - apollo-server-core "2.6.7" + apollo-engine-reporting-protobuf "0.4.0" + apollo-server-caching "0.5.0" + apollo-server-env "2.4.0" -apollo-server@~2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.6.tgz#0570fce4a682eb1de8bc1b86dbe2543de440cd4e" - integrity sha512-7Bulb3RnOO4/SGA66LXu3ZHCXIK8MYMrsxy4yti1/adDIUmcniolDqJwOYUGoTmv1AQjRxgJb4TVZ0Dk9nrrYg== +apollo-server@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.9.tgz#10e70488b35bf5171612dfd3f030e4ef94c75295" + integrity sha512-thZxUHVM1CLl3503gMCVirxN9J/33s5C1R+hHMEfLaUSoDlXSMA81Y9LCOi9+6d0C9l5DwiZCFXeZI/fKic2RA== dependencies: - apollo-server-core "2.6.6" - apollo-server-express "2.6.6" + apollo-server-core "2.6.9" + apollo-server-express "2.6.9" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.3.tgz#8533e3e2dca2d5a25e8439ce498ea33ff4d159ee" - integrity sha512-H6fSC+awQGnfDyYdGIB0UQUhcUC3n5Vy+ujacJ0bY6R+vwWeZOQvu7wRHNjk/rbOSTLCo9A0OcVX7huRyu9SZg== +apollo-tracing@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.4.tgz#f24d1065100b6d8bf581202859ea0e85ba7bf30d" + integrity sha512-vA0FJCBkFpwdWyVF5UtCqN+enShejyiqSGqq8NxXHU1+GEYTngWa56x9OGsyhX+z4aoDIa3HPKPnP3pjzA0qpg== dependencies: apollo-server-env "2.4.0" - graphql-extensions "0.7.4" + graphql-extensions "0.7.7" -apollo-tracing@^0.1.0: - version "0.1.4" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7" - integrity sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ== +apollo-tracing@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.0.tgz#28cd9c61a4db12b2c24dad67fdedd309806c1650" + integrity sha512-cNOtOlyZ56iJRsCjnxjM1V0SnQ2ZZttuyoeOejdat6llPfk5bfYTVOKMjdbSfDvU33LS9g9sqNJCT0MwrEPFKQ== dependencies: - graphql-extensions "~0.0.9" - -apollo-upload-server@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/apollo-upload-server/-/apollo-upload-server-7.1.0.tgz#21e07b52252b3749b913468599813e13cfca805f" - integrity sha512-cD9ReCeyurYwZyEDqJYb5TOc9dt8yhPzS+MtrY3iJdqw+pqiiyPngAvVXHjN+Ca7Lajvom4/AT/PBrYVDMM3Kw== - dependencies: - busboy "^0.2.14" - fs-capacitor "^1.0.0" - http-errors "^1.7.0" - object-path "^0.11.4" + apollo-server-env "2.4.0" + graphql-extensions "0.8.0" apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" @@ -1857,30 +1831,6 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -aws-lambda@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/aws-lambda/-/aws-lambda-0.1.2.tgz#19b1585075df31679597b976a5f1def61f12ccee" - integrity sha1-GbFYUHXfMWeVl7l2pfHe9h8SzO4= - dependencies: - aws-sdk "^*" - commander "^2.5.0" - dotenv "^0.4.0" - -aws-sdk@^*: - version "2.373.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.373.0.tgz#fcc5606634b3b11d80810ad252d1b52b3733d780" - integrity sha512-NZYXwXGtFt9jxaKXc+PJsLPnpbD03t0MAZRxh93g36kbFMuRXtY8CDqHYNQ0ZcrgQpXbCQiz1fxT5/wu5Cu70g== - dependencies: - buffer "4.9.1" - events "1.1.1" - ieee754 "1.1.8" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.1.0" - xml2js "0.4.19" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -1970,11 +1920,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.0.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" - integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== - base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -2043,14 +1988,7 @@ bn.js@^2.0.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625" integrity sha1-EhYrwq5x/EClYmwzQ486h1zTdiU= -body-parser-graphql@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/body-parser-graphql/-/body-parser-graphql-1.1.0.tgz#80a80353c7cb623562fd375750dfe018d75f0f7c" - integrity sha512-bOBF4n1AnUjcY1SzLeibeIx4XOuYqEkjn/Lm4yKhnN6KedoXMv4hVqgcKHGRnxOMJP64tErqrQU+4cihhpbJXg== - dependencies: - body-parser "^1.18.2" - -body-parser@1.19.0, body-parser@^1.18.2, body-parser@^1.18.3: +body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -2161,28 +2099,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer@4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= -busboy@^0.2.14: - version "0.2.14" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" - integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= - dependencies: - dicer "0.2.5" - readable-stream "1.1.x" - busboy@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" @@ -2416,7 +2337,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.5.0, commander@^2.8.1, commander@^2.9.0: +commander@^2.8.1, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -2528,17 +2449,12 @@ core-js-pure@3.1.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.2.tgz#62fc435f35b7374b9b782013cdcb2f97e9f6dffa" integrity sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w== -core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7: - version "2.6.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944" - integrity sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g== +core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.5: + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0.tgz#a8dbfa978d29bfc263bfb66c556d0ca924c28957" - integrity sha512-WBmxlgH2122EzEJ6GH8o9L/FeoUKxxxZ6q6VUxoTlsE4EvbTWKJb447eyVxTEuq0LpXjlq/kCB2qgBvsYRkLvQ== - -core-js@^3.0.1: +core-js@^3.0.0, core-js@^3.0.1: version "3.1.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.3.tgz#95700bca5f248f5f78c0ec63e784eca663ec4138" integrity sha512-PWZ+ZfuaKf178BIAg+CRsljwjIMRV8MY00CbZczkR6Zk5LfkSkjGoaab3+bqRQWVITNZxQB7TFYz+CFcyuamvA== @@ -2833,14 +2749,6 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= -dicer@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" - integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= - dependencies: - readable-stream "1.1.x" - streamsearch "0.1.2" - dicer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" @@ -2933,11 +2841,6 @@ dot-prop@^4.1.0: dependencies: is-obj "^1.0.0" -dotenv@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-0.4.0.tgz#f6fb351363c2d92207245c737802c9ab5ae1495a" - integrity sha1-9vs1E2PC2SIHJFxzeALJq1rhSVo= - dotenv@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" @@ -3180,10 +3083,10 @@ eslint-plugin-import@~2.18.0: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~22.7.2: - version "22.7.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.2.tgz#7ab118a66a34e46ae5e16a128b5d24fd28b43dca" - integrity sha512-Aecqe3ulBVI7amgOycVI8ZPL8o0SnGHOf3zn2/Ciu8TXyXDHcjtwD3hOs3ss/Qh/VAwlW/DMcuiXg5btgF+XMA== +eslint-plugin-jest@~22.9.0: + version "22.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.9.0.tgz#2573dbcb4f1066b96a6e6d3b9aa439c80b28975a" + integrity sha512-V89BUiwf76FHlhj1mlNhNyvpzTy8VbWCh2RZpKYz/XDSl/pcuwFiE/LMt7r3q1sRKygzEMjbYeDob8MMuvakXg== eslint-plugin-node@~9.1.0: version "9.1.0" @@ -3335,11 +3238,6 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - exec-sh@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" @@ -3406,7 +3304,7 @@ expect@^24.8.0: jest-message-util "^24.8.0" jest-regex-util "^24.3.0" -express@^4.0.0, express@^4.16.3, express@~4.17.1: +express@^4.0.0, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== @@ -3661,11 +3559,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-capacitor@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0" - integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ== - fs-capacitor@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" @@ -3842,79 +3735,35 @@ graphql-custom-directives@~0.2.14: moment "^2.22.2" numeral "^2.0.6" -graphql-deduplicator@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3" - integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA== - -graphql-extensions@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.4.tgz#78327712822281d5778b9210a55dc59c93a9c184" - integrity sha512-Ly+DiTDU+UtlfPGQkqmBX2SWMr9OT3JxMRwpB9K86rDNDBTJtG6AE2kliQKKE+hg1+945KAimO7Ep+YAvS7ywg== +graphql-extensions@0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.7.tgz#19f4dea35391065de72b25def98f8396887bdf43" + integrity sha512-xiTbVGPUpLbF86Bc+zxI/v/axRkwZx3s+y2/kUb2c2MxNZeNhMZEw1dSutuhY2f2JkRkYFJii0ucjIVqPAQ/Lg== dependencies: "@apollographql/apollo-tools" "^0.3.6" + apollo-server-env "2.4.0" -graphql-extensions@0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.5.tgz#fab2b9e53cf6014952e6547456d50680ff0ea579" - integrity sha512-B1m+/WEJa3IYKWqBPS9W/7OasfPmlHOSz5hpEAq2Jbn6T0FQ/d2YWFf2HBETHR3RR2qfT+55VMiYovl2ga3qcg== +graphql-extensions@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.8.0.tgz#b3fe7915aa84eef5a39135840840cc4d2e700c46" + integrity sha512-zV9RefkusIXqi9ZJtl7IJ5ecjDKdb7PLAb5E3CmxX3OK1GwNCIubp0vE7Fp4fXlCUKgTB1Woubs0zj71JT8o0A== dependencies: "@apollographql/apollo-tools" "^0.3.6" - -graphql-extensions@0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.6.tgz#80cdddf08b0af12525529d1922ee2ea0d0cc8ecf" - integrity sha512-RV00O3YFD1diehvdja180BlKOGWgeigr/8/Wzr6lXwLcFtk6FecQC/7nf6oW1qhuXczHyNjt/uCr0WWbWq6mYg== - dependencies: - "@apollographql/apollo-tools" "^0.3.6" - -graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: - version "0.0.10" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d" - integrity sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA== - dependencies: - core-js "^2.5.3" - source-map-support "^0.5.1" - -graphql-import@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223" - integrity sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw== - dependencies: - lodash "^4.17.4" - resolve-from "^4.0.0" + apollo-server-env "2.4.0" + apollo-server-types "0.2.0" graphql-iso-date@~3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96" integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q== -graphql-middleware@3.0.2, graphql-middleware@~3.0.2: +graphql-middleware@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-3.0.2.tgz#c8cdb67615eec02aec237b455e679f5fc973ddc4" integrity sha512-sRqu1sF+77z42z1OVM1QDHKQWnWY5K3nAgqWiZwx3U4tqNZprrDuXxSChPMliV343IrVkpYdejUYq9w24Ot3FA== dependencies: graphql-tools "^4.0.4" -graphql-playground-html@1.6.12: - version "1.6.12" - resolved "https://registry.yarnpkg.com/graphql-playground-html/-/graphql-playground-html-1.6.12.tgz#8b3b34ab6013e2c877f0ceaae478fafc8ca91b85" - integrity sha512-yOYFwwSMBL0MwufeL8bkrNDgRE7eF/kTHiwrqn9FiR9KLcNIl1xw9l9a+6yIRZM56JReQOHpbQFXTZn1IuSKRg== - -graphql-playground-middleware-express@1.7.11: - version "1.7.11" - resolved "https://registry.yarnpkg.com/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.11.tgz#bbffd784a37133bfa7165bdd8f429081dbf4bcf6" - integrity sha512-sKItB4s3FxqlwCgXdMfwRAfssSoo31bcFsGAAg/HzaZLicY6CDlofKXP8G5iPDerB6NaoAcAaBLutLzl9sd4fQ== - dependencies: - graphql-playground-html "1.6.12" - -graphql-playground-middleware-lambda@1.7.12: - version "1.7.12" - resolved "https://registry.yarnpkg.com/graphql-playground-middleware-lambda/-/graphql-playground-middleware-lambda-1.7.12.tgz#1b06440a288dbcd53f935b43e5b9ca2738a06305" - integrity sha512-fJ1Y0Ck5ctmfaQFoWv7vNnVP7We19P3miVmOT85YPrjpzbMYv0wPfxm4Zjt8nnqXr0KU9nGW53tz3K7/Lvzxtw== - dependencies: - graphql-playground-html "1.6.12" - graphql-request@~1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-1.8.2.tgz#398d10ae15c585676741bde3fc01d5ca948f8fbe" @@ -3932,13 +3781,6 @@ graphql-shield@~6.0.3: object-hash "^1.3.1" yup "^0.27.0" -graphql-subscriptions@^0.5.8: - version "0.5.8" - resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz#13a6143c546bce390404657dc73ca501def30aa7" - integrity sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ== - dependencies: - iterall "^1.2.1" - graphql-subscriptions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8" @@ -3962,7 +3804,7 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.4: iterall "^1.1.3" uuid "^3.1.0" -graphql-upload@^8.0.0, graphql-upload@^8.0.2: +graphql-upload@^8.0.2: version "8.0.7" resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.7.tgz#8644264e241529552ea4b3797e7ee15809cf01a3" integrity sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g== @@ -3972,35 +3814,7 @@ graphql-upload@^8.0.0, graphql-upload@^8.0.2: http-errors "^1.7.2" object-path "^0.11.4" -graphql-yoga@~1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.18.0.tgz#2668278e94a0bd1b2ff8c60f928c4e18d62e381a" - integrity sha512-WEibitQA2oFTmD7XBO8/ps8DWeVpkzOzgbB3EvtM2oIpyGhPCzRZYrC7OS9MmijvRwLRXsgHImHWUm82ZrIOWA== - dependencies: - "@types/cors" "^2.8.4" - "@types/express" "^4.11.1" - "@types/graphql" "^14.0.0" - "@types/graphql-deduplicator" "^2.0.0" - "@types/zen-observable" "^0.5.3" - apollo-server-express "^1.3.6" - apollo-server-lambda "1.3.6" - apollo-upload-server "^7.0.0" - aws-lambda "^0.1.2" - body-parser-graphql "1.1.0" - cors "^2.8.4" - express "^4.16.3" - graphql "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" - graphql-deduplicator "^2.0.1" - graphql-import "^0.7.0" - graphql-middleware "3.0.2" - graphql-playground-middleware-express "1.7.11" - graphql-playground-middleware-lambda "1.7.12" - graphql-subscriptions "^0.5.8" - graphql-tools "^4.0.0" - graphql-upload "^8.0.0" - subscriptions-transport-ws "^0.9.8" - -"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.4.2: +graphql@^14.2.1, graphql@~14.4.2: version "14.4.2" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3" integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ== @@ -4207,17 +4021,6 @@ http-errors@1.7.2, http-errors@^1.7.2, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" - integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -4234,16 +4037,6 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" - integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q= - -ieee754@^1.1.4: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== - ienoopen@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" @@ -4310,7 +4103,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= @@ -4604,11 +4397,6 @@ is-windows@^1.0.0, is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -5045,12 +4833,7 @@ jest@~24.8.0: import-local "^2.0.0" jest-cli "^24.8.0" -jmespath@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= - -joi@^13.0.0, joi@^13.7.0: +joi@^13.7.0: version "13.7.0" resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== @@ -5409,9 +5192,9 @@ lodash.isstring@^4.0.1: integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= lodash.mergewith@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" - integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== lodash.once@^4.0.0: version "4.1.1" @@ -5428,10 +5211,10 @@ lodash@=3.10.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= -lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== +lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@~4.17.14: + version "4.17.14" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" + integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== long@^4.0.0: version "4.0.0" @@ -5864,10 +5647,10 @@ node-releases@^1.1.19: dependencies: semver "^5.3.0" -nodemailer@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3" - integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g== +nodemailer@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.0.tgz#a89b0c62d3937bdcdeecbf55687bd7911b627e12" + integrity sha512-TEHBNBPHv7Ie/0o3HXnb7xrPSSQmH1dXwQKRaMKDBGt/ZN54lvDVujP6hKkO/vjkIYL9rK8kHSG11+G42Nhxuw== nodemon@~1.19.1: version "1.19.1" @@ -6527,11 +6310,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -6552,11 +6330,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -6641,16 +6414,6 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -readable-stream@1.1.x: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.3, readable-stream@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -7000,11 +6763,6 @@ sanitize-html@~1.20.1: srcset "^1.0.0" xtend "^4.0.1" -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= - sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -7104,11 +6862,6 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" @@ -7221,7 +6974,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.1, source-map-support@^0.5.6, source-map-support@^0.5.9: +source-map-support@^0.5.6, source-map-support@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== @@ -7427,11 +7180,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.1.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -7475,7 +7223,7 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.8: +subscriptions-transport-ws@^0.9.11: version "0.9.15" resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d" integrity sha512-f9eBfWdHsePQV67QIX+VRhf++dn1adyC/PZHP6XI5AfKnZ4n0FW+v5omxwdHVpd4xq2ZijaHEcmlQrhBY79ZWQ== @@ -7906,14 +7654,6 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -7949,11 +7689,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== - uuid@^3.1.0, uuid@^3.3.2, uuid@~3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -8064,13 +7799,13 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -wait-on@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.2.0.tgz#c83924df0fc42a675c678324c49c769d378bcb85" - integrity sha512-QUGNKlKLDyY6W/qHdxaRlXUAgLPe+3mLL/tRByHpRNcHs/c7dZXbu+OnJWGNux6tU1WFh/Z8aEwvbuzSAu79Zg== +wait-on@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082" + integrity sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ== dependencies: - core-js "^2.5.7" - joi "^13.0.0" + "@hapi/joi" "^15.0.3" + core-js "^2.6.5" minimist "^1.2.0" request "^2.88.0" rx "^4.1.0" @@ -8225,7 +7960,7 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@0.4.19, xml2js@^0.4.17: +xml2js@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature index e55535ea9..96196be01 100644 --- a/cypress/integration/administration/TagsAndCategories.feature +++ b/cypress/integration/administration/TagsAndCategories.feature @@ -22,16 +22,16 @@ Feature: Tags and Categories When I navigate to the administration dashboard And I click on the menu item "Categories" Then I can see the following table: - | | Name | Posts | - | | Just For Fun | 2 | - | | Happyness & Values | 1 | - | | Health & Wellbeing | 0 | + | | Name | Posts | + | | Just For Fun | 2 | + | | Happyness & Values | 1 | + | | Health & Wellbeing | 0 | Scenario: See an overview of tags When I navigate to the administration dashboard And I click on the menu item "Tags" Then I can see the following table: - | | Name | Users | Posts | - | 1 | Democracy | 3 | 4 | - | 2 | Nature | 2 | 3 | - | 3 | Ecology | 1 | 1 | + | | Name | Users | Posts | + | 1 | Democracy | 3 | 4 | + | 2 | Nature | 2 | 3 | + | 3 | Ecology | 1 | 1 | diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js index 1df1e2652..b1bf9e4e0 100644 --- a/cypress/integration/common/profile.js +++ b/cypress/integration/common/profile.js @@ -1,36 +1,36 @@ -import { When, Then } from 'cypress-cucumber-preprocessor/steps' +import { When, Then } from "cypress-cucumber-preprocessor/steps"; /* global cy */ -When('I visit my profile page', () => { - cy.openPage('profile/peter-pan') -}) +When("I visit my profile page", () => { + cy.openPage("profile/peter-pan"); +}); -Then('I should be able to change my profile picture', () => { - const avatarUpload = 'onourjourney.png' +Then("I should be able to change my profile picture", () => { + const avatarUpload = "onourjourney.png"; - cy.fixture(avatarUpload, 'base64').then(fileContent => { - cy.get('#customdropzone').upload( - { fileContent, fileName: avatarUpload, mimeType: 'image/png' }, - { subjectType: 'drag-n-drop' } - ) - }) - cy.get('.profile-avatar img') - .should('have.attr', 'src') - .and('contains', 'onourjourney') - cy.contains('.iziToast-message', 'Upload successful').should( - 'have.length', + cy.fixture(avatarUpload, "base64").then(fileContent => { + cy.get("#customdropzone").upload( + { fileContent, fileName: avatarUpload, mimeType: "image/png" }, + { subjectType: "drag-n-drop", force: true } + ); + }); + cy.get(".profile-avatar img") + .should("have.attr", "src") + .and("contains", "onourjourney"); + cy.contains(".iziToast-message", "Upload successful").should( + "have.length", 1 - ) -}) + ); +}); When("I visit another user's profile page", () => { - cy.openPage('profile/peter-pan') -}) + cy.openPage("profile/peter-pan"); +}); -Then('I cannot upload a picture', () => { - cy.get('.ds-card-content') +Then("I cannot upload a picture", () => { + cy.get(".ds-card-content") .children() - .should('not.have.id', 'customdropzone') - .should('have.class', 'ds-avatar') -}) + .should("not.have.id", "customdropzone") + .should("have.class", "ds-avatar"); +}); diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index f8d18baa0..f996db992 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -276,9 +276,9 @@ When("I fill the password form with:", table => { table = table.rowsHash(); cy.get("input[id=oldPassword]") .type(table["Your old password"]) - .get("input[id=newPassword]") + .get("input[id=password]") .type(table["Your new passsword"]) - .get("input[id=confirmPassword]") + .get("input[id=passwordConfirmation]") .type(table["Confirm new password"]); }); diff --git a/cypress/support/factories.js b/cypress/support/factories.js index dd16e8198..825f65b83 100644 --- a/cypress/support/factories.js +++ b/cypress/support/factories.js @@ -23,24 +23,27 @@ Cypress.Commands.add('factory', () => { Cypress.Commands.add( 'create', { prevSubject: true }, - (factory, node, properties) => { - return factory.create(node, properties) + async (factory, node, properties) => { + await factory.create(node, properties) + return factory } ) Cypress.Commands.add( 'relate', { prevSubject: true }, - (factory, node, relationship, properties) => { - return factory.relate(node, relationship, properties) + async (factory, node, relationship, properties) => { + await factory.relate(node, relationship, properties) + return factory } ) Cypress.Commands.add( 'mutate', { prevSubject: true }, - (factory, mutation, variables) => { - return factory.mutate(mutation, variables) + async (factory, mutation, variables) => { + await factory.mutate(mutation, variables) + return factory } ) diff --git a/deployment/human-connection/templates/configmap.template.yaml b/deployment/human-connection/templates/configmap.template.yaml index 2b7ffeeb8..1e8b37b06 100644 --- a/deployment/human-connection/templates/configmap.template.yaml +++ b/deployment/human-connection/templates/configmap.template.yaml @@ -4,14 +4,10 @@ data: SMTP_HOST: "mailserver.human-connection" SMTP_PORT: "25" - SMTP_USERNAME: "" - SMTP_PASSWORD: "" GRAPHQL_PORT: "4000" GRAPHQL_URI: "http://nitro-backend.human-connection:4000" MOCKS: "false" NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687" - NEO4J_USERNAME: "neo4j" - NEO4J_PASSWORD: "neo4j" NEO4J_AUTH: "none" CLIENT_URI: "https://nitro-staging.human-connection.org" metadata: diff --git a/deployment/human-connection/templates/secrets.template.yaml b/deployment/human-connection/templates/secrets.template.yaml index 9f59b948a..6a6206189 100644 --- a/deployment/human-connection/templates/secrets.template.yaml +++ b/deployment/human-connection/templates/secrets.template.yaml @@ -5,11 +5,10 @@ data: MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA==" PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4" MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK" - SMTP_HOST: - SMTP_PORT: 587 SMTP_USERNAME: SMTP_PASSWORD: - SMTP_IGNORE_TLS: + NEO4J_USERNAME: + NEO4J_PASSWORD: metadata: name: human-connection namespace: human-connection diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql index 027cea019..adf63dc1f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql @@ -25,7 +25,7 @@ [?] type: String, // in nitro this is a defined enum - seems good for now [X] required: true }, -[X] key: { +[X] id: { [X] type: String, [X] required: true }, @@ -43,7 +43,7 @@ CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge MERGE(b:Badge {id: badge._id["$oid"]}) ON CREATE SET -b.key = badge.key, +b.id = badge.key, b.type = badge.type, b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''), b.status = badge.status, diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql index e69de29bb..18fb6699f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql @@ -0,0 +1 @@ +MATCH (u:User)-[e:EMOTED]->(c:Post) DETACH DELETE e; \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql index 8aad9e923..06341f277 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql @@ -5,31 +5,54 @@ // [-] Omitted in Nitro // [?] Unclear / has work to be done for Nitro { -[ ] userId: { -[ ] type: String, -[ ] required: true, +[X] userId: { +[X] type: String, +[X] required: true, [-] index: true }, -[ ] contributionId: { -[ ] type: String, -[ ] required: true, +[X] contributionId: { +[X] type: String, +[X] required: true, [-] index: true }, -[ ] rated: { -[ ] type: String, +[?] rated: { +[X] type: String, [ ] required: true, -[ ] enum: ['funny', 'happy', 'surprised', 'cry', 'angry'] +[?] enum: ['funny', 'happy', 'surprised', 'cry', 'angry'] }, -[ ] createdAt: { -[ ] type: Date, -[ ] default: Date.now +[X] createdAt: { +[X] type: Date, +[X] default: Date.now }, -[ ] updatedAt: { -[ ] type: Date, -[ ] default: Date.now +[X] updatedAt: { +[X] type: Date, +[X] default: Date.now }, -[ ] wasSeeded: { type: Boolean } +[-] wasSeeded: { type: Boolean } } */ -CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion; +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion +MATCH (u:User {id: emotion.userId}), + (c:Post {id: emotion.contributionId}) +MERGE (u)-[e:EMOTED { + id: emotion._id["$oid"], + emotion: emotion.rated, + createdAt: datetime(emotion.createdAt.`$date`), + updatedAt: datetime(emotion.updatedAt.`$date`) + }]->(c) +RETURN e; +/* + // Queries + // user sets an emotion emotion: + // MERGE (u)-[e:EMOTED {id: ..., emotion: "funny", createdAt: ..., updatedAt: ...}]->(c) + // user removes emotion + // MATCH (u)-[e:EMOTED]->(c) DELETE e + // contribution distributions over every `emotion` property value for one post + // MATCH (u:User)-[e:EMOTED]->(c:Post {id: "5a70bbc8508f5b000b443b1a"}) RETURN e.emotion,COUNT(e.emotion) + // contribution distributions over every `emotion` property value for one user (advanced - "whats the primary emotion used by the user?") + // MATCH (u:User{id:"5a663b1ac64291000bf302a1"})-[e:EMOTED]->(c:Post) RETURN e.emotion,COUNT(e.emotion) + // contribution distributions over every `emotion` property value for all posts created by one user (advanced - "how do others react to my contributions?") + // MATCH (u:User)-[e:EMOTED]->(c:Post)<-[w:WROTE]-(a:User{id:"5a663b1ac64291000bf302a1"}) RETURN e.emotion,COUNT(e.emotion) + // if we can filter the above an a variable timescale that would be great (should be possible on createdAt and updatedAt fields) +*/ \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql index 3624448c3..3de01f8ea 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql @@ -1 +1 @@ -// this is just a relation between users(?) - no need to delete \ No newline at end of file +MATCH (u1:User)-[f:FOLLOWS]->(u2:User) DETACH DELETE f; \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh index 8eef68c92..cef2846a7 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh @@ -60,8 +60,8 @@ delete_collection "contributions" "contributions_post" delete_collection "contributions" "contributions_cando" delete_collection "shouts" "shouts" delete_collection "comments" "comments" +delete_collection "emotions" "emotions" -#delete_collection "emotions" #delete_collection "invites" #delete_collection "notifications" #delete_collection "organizations" @@ -82,12 +82,12 @@ import_collection "users" "users/users.cql" import_collection "follows_users" "follows/follows.cql" #import_collection "follows_organizations" "follows/follows.cql" import_collection "contributions_post" "contributions/contributions.cql" -import_collection "contributions_cando" "contributions/contributions.cql" +#import_collection "contributions_cando" "contributions/contributions.cql" #import_collection "contributions_DELETED" "contributions/contributions.cql" import_collection "shouts" "shouts/shouts.cql" import_collection "comments" "comments/comments.cql" +import_collection "emotions" "emotions/emotions.cql" -# import_collection "emotions" # import_collection "invites" # import_collection "notifications" # import_collection "organizations" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql index 4d7c9aa9f..7574fd3b2 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql @@ -101,7 +101,7 @@ ON CREATE SET u.name = user.name, u.slug = user.slug, u.email = user.email, -u.password = user.password, +u.encryptedPassword = user.password, u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''), u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''), u.wasInvited = user.wasInvited, @@ -111,6 +111,13 @@ u.createdAt = user.createdAt.`$date`, u.updatedAt = user.updatedAt.`$date`, u.deleted = user.deletedAt IS NOT NULL, u.disabled = false +MERGE (e:EmailAddress { + email: user.email, + createdAt: toString(datetime()), + verifiedAt: toString(datetime()) +}) +MERGE (e)-[:BELONGS_TO]->(u) +MERGE (u)<-[:PRIMARY_EMAIL]-(e) WITH u, user, user.badgeIds AS badgeIds UNWIND badgeIds AS badgeId MATCH (b:Badge {id: badgeId}) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index bc627a67a..0c6576ca7 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -26,9 +26,4 @@ services: ports: - 4001:4001 - 4123:4123 - neo4j: - environment: - - NEO4J_AUTH=none - ports: - - 7687:7687 - - 7474:7474 + diff --git a/docker-compose.yml b/docker-compose.yml index ca66217c2..93e80d9c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: networks: - hc-network environment: + - NUXT_BUILD=.nuxt-dist - HOST=0.0.0.0 - GRAPHQL_URI=http://backend:4000 - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ" diff --git a/neo4j/db_setup.sh b/neo4j/db_setup.sh index 21ed54571..d4c7b9af8 100755 --- a/neo4j/db_setup.sh +++ b/neo4j/db_setup.sh @@ -34,6 +34,8 @@ CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE; + +CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE; ' | cypher-shell echo ' diff --git a/package.json b/package.json index bfec1d73a..87ec4e9ef 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,9 @@ "bcryptjs": "^2.4.3", "codecov": "^3.5.0", "cross-env": "^5.2.0", - "cypress": "^3.3.2", + "cypress": "^3.4.0", "cypress-cucumber-preprocessor": "^1.12.0", - "cypress-file-upload": "^3.2.0", + "cypress-file-upload": "^3.3.2", "cypress-plugin-retries": "^1.2.2", "dotenv": "^8.0.0", "faker": "Marak/faker.js#master", diff --git a/webapp/.gitignore b/webapp/.gitignore index f8c980f7c..bc179d78a 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -61,6 +61,8 @@ typings/ # nuxt.js build output .nuxt +# also the build output in docker container +.nuxt-dist # Nuxt generate dist diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index 560249b4a..11652fad0 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -66,7 +66,8 @@ blockquote { border-left: 3px dotted $color-neutral-70; &::before { - content: '\201C'; /*Unicode for Left Double Quote*/ + content: '\201C'; + /*Unicode for Left Double Quote*/ /*Font*/ font-size: $font-size-xxxx-large; diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index 42ac23e4d..936d13adb 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -1,6 +1,6 @@ diff --git a/webapp/components/Editor/spec.js b/webapp/components/Editor/Editor.spec.js similarity index 94% rename from webapp/components/Editor/spec.js rename to webapp/components/Editor/Editor.spec.js index b982d941d..d457609bd 100644 --- a/webapp/components/Editor/spec.js +++ b/webapp/components/Editor/Editor.spec.js @@ -1,5 +1,5 @@ import { mount, createLocalVue } from '@vue/test-utils' -import Editor from './' +import Editor from './Editor' import Vuex from 'vuex' import Styleguide from '@human-connection/styleguide' @@ -36,7 +36,9 @@ describe('Editor.vue', () => { propsData, localVue, sync: false, - stubs: { transition: false }, + stubs: { + transition: false, + }, store, })) } diff --git a/webapp/components/Editor/index.vue b/webapp/components/Editor/Editor.vue similarity index 67% rename from webapp/components/Editor/index.vue rename to webapp/components/Editor/Editor.vue index 84649f436..4413bfa0d 100644 --- a/webapp/components/Editor/index.vue +++ b/webapp/components/Editor/Editor.vue @@ -1,18 +1,51 @@