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 0b9e25447..48fc5e787 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,7 @@ "apollo-client": "~2.6.3", "apollo-link-context": "~1.0.18", "apollo-link-http": "~1.5.15", - "apollo-server": "~2.6.8", + "apollo-server": "~2.6.9", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", @@ -68,20 +67,20 @@ "helmet": "~3.18.0", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", - "lodash": "~4.17.13", + "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", @@ -90,7 +89,7 @@ "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/preset-env": "~7.5.4", "@babel/register": "~7.4.4", - "apollo-server-testing": "~2.6.8", + "apollo-server-testing": "~2.6.9", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.2", "babel-jest": "~24.8.0", 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/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index a6b6ef0da..31c373fc7 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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, DeleteSocialMedia: isAuthenticated, // AddBadgeRewarded: isAdmin, diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 9ac15a60f..77282b6b5 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,36 @@ 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) + } +} + export default { Mutation: { CreateSocialMedia: validate(socialMediaSchema), + CreateComment: validateCommentCreation, }, } 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/User.js b/backend/src/models/User.js index d8f768ae9..02ce04513 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -43,6 +43,12 @@ module.exports = { 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: { diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 0e6ae5864..09d1dbbeb 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -1,6 +1,7 @@ // 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/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/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..7dcca6eb8 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,12 +1,20 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' import { host, login } from '../../jest/helpers' +import gql from 'graphql-tag' 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 +30,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 +42,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 +70,94 @@ 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 expected = { - reward: 'u1', + reward: { + id: 'u1', + badges: [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }], + }, } - 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/users.js b/backend/src/schema/resolvers/users.js index 2d9282b60..ea076d005 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -139,7 +139,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..fa83017da 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -147,7 +147,7 @@ describe('users', () => { } ` beforeEach(async () => { - asAuthor = await factory.create('User', { + await factory.create('User', { email: 'test@example.org', password: '1234', id: 'u343', @@ -191,6 +191,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/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 a735e90e5..30baa65a3 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -73,6 +73,8 @@ type User { badges: [Badge]! @relation(name: "REWARDED", direction: "IN") badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + + emotions: [EMOTED] } 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..8bdf03b9f 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 = { @@ -21,8 +21,7 @@ export default function create(params) { ...args, } args = await encryptPassword(args) - const user = await neodeInstance.create('User', args) - return user.toJson() + return neodeInstance.create('User', args) }, } } diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index bd12549be..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', @@ -123,30 +113,16 @@ import Factory from './factories' ]) 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', - }), + 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([ f.relate('User', 'Friends', { from: 'u1', to: 'u2', diff --git a/backend/yarn.lock b/backend/yarn.lock index 398bcc61a..1df53a7bb 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" @@ -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== @@ -1343,13 +1343,13 @@ 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" @@ -1406,17 +1406,17 @@ apollo-engine-reporting-protobuf@0.3.1: dependencies: 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-env@0.5.1: version "0.5.1" @@ -1486,50 +1486,24 @@ apollo-server-caching@0.4.0: dependencies: lru-cache "^5.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.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.5" + 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.6" - 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.6" - graphql-subscriptions "^1.0.0" - graphql-tag "^2.9.2" - graphql-tools "^4.0.0" - graphql-upload "^8.0.2" - sha.js "^2.4.11" - subscriptions-transport-ws "^0.9.11" - ws "^6.0.0" - -apollo-server-core@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.8.tgz#c8758b5f26b5f3b9fef51b911265b80a9ce5251d" - integrity sha512-Jxw+6R/2I2LiZ6kjRFTzPpdjw7HfuVLfNU+svgNlxioLducxBH/wqUs3qYTf4eVUUtWy+nSS/BUf/Ullo+Ur0Q== - dependencies: - "@apollographql/apollo-tools" "^0.3.6" - "@apollographql/graphql-playground-html" "1.6.20" - "@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-server-env "2.4.0" - apollo-server-errors "2.3.0" - apollo-server-plugin-base "0.5.7" - apollo-tracing "0.7.3" - fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.7.6" + graphql-extensions "0.7.7" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" @@ -1555,23 +1529,23 @@ 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.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.8.tgz#9f3e29f7087af669f05d75dfd335b4a9383ba48e" - integrity sha512-LQzVHknQDkHWffc2qK9dr/qNxQ/WecSKiye5/w10tXrOy3aruTFe67ysG/vMnFZ/puroqiZ2njHzhHZztqQ4sA== +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.8" + apollo-server-core "2.6.9" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" @@ -1599,41 +1573,36 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^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.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-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.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.7.tgz#267faeb5c2de7fa8d3be469cb99f82f601be7aed" - integrity sha512-HeEwEZ92c2XYRV+0CFLbstW3vUJ4idCxR9E9Q3wwvhXrq8gaGzqyDoC8EzAzRxCJUKcEn7xQOpT/AUTC/KtkRA== - -apollo-server-testing@~2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.8.tgz#e75364df7fdc2d6a11023f8a0f72a14309b90800" - integrity sha512-pch2I+8QhdXBMnGDctVth4BcZ5hocwY/ogtBMoQuv7H2HBnlDOz7dCM9BH4TW3+Tk6cFgvLTaDtLJ+NxMCtyVA== +apollo-server-testing@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.9.tgz#6c1d20a89c0676bf32714405d729c302d62adfb1" + integrity sha512-MQfXAjNsI63O9sY60tQnGy102sqJSr++Yzm+IR44WrK3Z7FHUDisoh6UATly04EDGtO034xtqukzdUNQCK7+rw== dependencies: - apollo-server-core "2.6.8" + apollo-server-core "2.6.9" -apollo-server@~2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.8.tgz#5f3cf5cf4f2feccbded0cb03fa37dcd8260e5c6a" - integrity sha512-BxwaGxnD3GPuZAAqsexVHFvDlF/s2X8pILgYQ4x+VhUkMeQ12DHQtKPuxn2v2GYwH0U/GDXNohkgwxF/5eTDsQ== +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.8" - apollo-server-express "2.6.8" + 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" @@ -2508,17 +2477,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.3, 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== @@ -3827,19 +3791,13 @@ graphql-deduplicator@^2.0.1: 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== - 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== +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.0.x, graphql-extensions@~0.0.9: version "0.0.10" @@ -5023,7 +4981,7 @@ jmespath@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== @@ -5382,9 +5340,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" @@ -5401,10 +5359,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.13: - version "4.17.13" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" - integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== +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.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" @@ -5837,10 +5795,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" @@ -8037,13 +7995,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" 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/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/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..84eb7074b 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, 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/package.json b/package.json index b93b154ac..b4447bdd2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "cross-env": "^5.2.0", "cypress": "^3.4.0", "cypress-cucumber-preprocessor": "^1.12.0", - "cypress-file-upload": "^3.2.0", + "cypress-file-upload": "^3.3.1", "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/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/FilterPosts/FilterPostsMenuItems.vue b/webapp/components/FilterPosts/FilterPostsMenuItems.vue new file mode 100644 index 000000000..593781cdb --- /dev/null +++ b/webapp/components/FilterPosts/FilterPostsMenuItems.vue @@ -0,0 +1,126 @@ + + + diff --git a/webapp/components/comments/CommentList/index.vue b/webapp/components/comments/CommentList/index.vue index ab5916629..db9a4974d 100644 --- a/webapp/components/comments/CommentList/index.vue +++ b/webapp/components/comments/CommentList/index.vue @@ -47,7 +47,8 @@ export default { }, watch: { Post(post) { - this.comments = post[0].comments || [] + const [first] = post + this.comments = (first && first.comments) || [] }, }, apollo: { diff --git a/webapp/components/notifications/NotificationMenu/index.vue b/webapp/components/notifications/NotificationMenu/index.vue index 20a9a7074..a7c0cc4b7 100644 --- a/webapp/components/notifications/NotificationMenu/index.vue +++ b/webapp/components/notifications/NotificationMenu/index.vue @@ -2,7 +2,7 @@ {{ totalNotifications }} - +