diff --git a/backend/Dockerfile b/backend/Dockerfile index 250ee845b..75f5a762f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -15,10 +15,10 @@ COPY .env.template .env CMD ["yarn", "run", "start"] FROM base as builder -RUN yarn install --frozen-lockfile --non-interactive +RUN yarn install --production=false --frozen-lockfile --non-interactive COPY . . RUN cp .env.template .env -RUN yarn run build +RUN NODE_ENV=production yarn run build # reduce image size with a multistage build FROM base as production diff --git a/backend/package.json b/backend/package.json index 5123e369b..dd75b54d9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -117,7 +117,7 @@ "chai": "~4.2.0", "cucumber": "~5.1.0", "eslint": "~6.3.0", - "eslint-config-prettier": "~6.2.0", + "eslint-config-prettier": "~6.3.0", "eslint-config-standard": "~14.1.0", "eslint-plugin-import": "~2.18.2", "eslint-plugin-jest": "~22.17.0", diff --git a/backend/src/middleware/filterBubble/filterBubble.spec.js b/backend/src/middleware/filterBubble/filterBubble.spec.js deleted file mode 100644 index 4dfcb76d1..000000000 --- a/backend/src/middleware/filterBubble/filterBubble.spec.js +++ /dev/null @@ -1,120 +0,0 @@ -import { gql } from '../../jest/helpers' -import Factory from '../../seed/factories' -import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' - -const factory = Factory() -const neode = getNeode() -const driver = getDriver() - -let authenticatedUser -let user -let query - -const currentUserParams = { - id: 'u1', - email: 'you@example.org', - name: 'This is you', - password: '1234', -} -const followedAuthorParams = { - id: 'u2', - email: 'followed@example.org', - name: 'Followed User', - password: '1234', -} -const randomAuthorParams = { - email: 'someone@example.org', - name: 'Someone else', - password: 'else', -} -const categoryIds = ['cat9'] - -beforeEach(async () => { - const [currentUser, followedAuthor, randomAuthor] = await Promise.all([ - factory.create('User', currentUserParams), - factory.create('User', followedAuthorParams), - factory.create('User', randomAuthorParams), - ]) - user = currentUser - await neode.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) - await currentUser.relateTo(followedAuthor, 'following') - await factory.create('Post', { - author: followedAuthor, - title: 'This is the post of a followed user', - categoryIds, - }) - await factory.create('Post', { - author: randomAuthor, - title: 'This is some random post', - categoryIds, - }) -}) - -beforeAll(() => { - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, - }) - const client = createTestClient(server) - query = client.query -}) - -afterEach(async () => { - await factory.cleanDatabase() -}) - -describe('Filter posts by author is followed by sb.', () => { - describe('given an authenticated user', () => { - beforeEach(async () => { - authenticatedUser = await user.toJson() - }) - - describe('no filter bubble', () => { - it('returns all posts', async () => { - const postQuery = gql` - { - Post(filter: {}) { - title - } - } - ` - const expected = { - data: { - Post: [ - { title: 'This is some random post' }, - { title: 'This is the post of a followed user' }, - ], - }, - } - await expect(query({ query: postQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('filtering for posts of followed users only', () => { - it('returns only posts authored by followed users', async () => { - const postQuery = gql` - { - Post(filter: { author: { followedBy_some: { id: "u1" } } }) { - title - } - } - ` - const expected = { - data: { Post: [{ title: 'This is the post of a followed user' }] }, - } - await expect(query({ query: postQuery })).resolves.toMatchObject(expected) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index af2d8089c..1fc8c51f6 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -56,7 +56,7 @@ beforeAll(() => { beforeEach(async () => { variables = {} user = await factory.create('User', { - id: 'u198', + id: 'current-user', name: 'TestUser', email: 'test@example.org', password: '1234', @@ -91,44 +91,63 @@ afterEach(async () => { }) describe('Post', () => { - const postQueryFilteredByCategories = gql` - query Post($filter: _PostFilter) { - Post(filter: $filter) { - id - categories { - id - } - } - } - ` - - const postQueryFilteredByEmotions = gql` - query Post($filter: _PostFilter) { - Post(filter: $filter) { - id - emotions { - emotion - } - } - } - ` - describe('can be filtered', () => { - let post31, post32 + let followedUser, happyPost, cryPost beforeEach(async () => { - ;[post31, post32] = await Promise.all([ - factory.create('Post', { id: 'p31', categoryIds: ['cat4'] }), - factory.create('Post', { id: 'p32', categoryIds: ['cat15'] }), - factory.create('Post', { id: 'p33', categoryIds: ['cat9'] }), + ;[followedUser] = await Promise.all([ + factory.create('User', { + id: 'followed-by-me', + email: 'followed@example.org', + name: 'Followed User', + password: '1234', + }), + ]) + ;[happyPost, cryPost] = await Promise.all([ + factory.create('Post', { id: 'happy-post', categoryIds: ['cat4'] }), + factory.create('Post', { id: 'cry-post', categoryIds: ['cat15'] }), + factory.create('Post', { + id: 'post-by-followed-user', + categoryIds: ['cat9'], + author: followedUser, + }), ]) }) + describe('no filter', () => { + it('returns all posts', async () => { + const postQueryNoFilters = gql` + query Post($filter: _PostFilter) { + Post(filter: $filter) { + id + } + } + ` + const expected = [{ id: 'happy-post' }, { id: 'cry-post' }, { id: 'post-by-followed-user' }] + variables = { filter: {} } + await expect(query({ query: postQueryNoFilters, variables })).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining(expected), + }, + }) + }) + }) + it('by categories', async () => { + const postQueryFilteredByCategories = gql` + query Post($filter: _PostFilter) { + Post(filter: $filter) { + id + categories { + id + } + } + } + ` const expected = { data: { Post: [ { - id: 'p33', + id: 'post-by-followed-user', categories: [{ id: 'cat9' }], }, ], @@ -140,45 +159,88 @@ describe('Post', () => { ).resolves.toMatchObject(expected) }) - it('by emotions', async () => { + describe('by emotions', () => { + const postQueryFilteredByEmotions = gql` + query Post($filter: _PostFilter) { + Post(filter: $filter) { + id + emotions { + emotion + } + } + } + ` + + it('filters by single emotion', async () => { + const expected = { + data: { + Post: [ + { + id: 'happy-post', + emotions: [{ emotion: 'happy' }], + }, + ], + }, + } + await user.relateTo(happyPost, 'emoted', { emotion: 'happy' }) + variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } } + await expect( + query({ query: postQueryFilteredByEmotions, variables }), + ).resolves.toMatchObject(expected) + }) + + it('filters by multiple emotions', async () => { + const expected = [ + { + id: 'happy-post', + emotions: [{ emotion: 'happy' }], + }, + { + id: 'cry-post', + emotions: [{ emotion: 'cry' }], + }, + ] + await user.relateTo(happyPost, 'emoted', { emotion: 'happy' }) + await user.relateTo(cryPost, 'emoted', { emotion: 'cry' }) + variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } } + await expect( + query({ query: postQueryFilteredByEmotions, variables }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining(expected), + }, + }) + }) + }) + + it('by followed-by', async () => { + const postQueryFilteredByUsersFollowed = gql` + query Post($filter: _PostFilter) { + Post(filter: $filter) { + id + author { + id + name + } + } + } + ` + + await user.relateTo(followedUser, 'following') + variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } } const expected = { data: { Post: [ { - id: 'p31', - emotions: [{ emotion: 'happy' }], + id: 'post-by-followed-user', + author: { id: 'followed-by-me', name: 'Followed User' }, }, ], }, } - await user.relateTo(post31, 'emoted', { emotion: 'happy' }) - variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } } - await expect(query({ query: postQueryFilteredByEmotions, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('supports filtering by multiple emotions', async () => { - const expected = [ - { - id: 'p31', - emotions: [{ emotion: 'happy' }], - }, - { - id: 'p32', - emotions: [{ emotion: 'cry' }], - }, - ] - await user.relateTo(post31, 'emoted', { emotion: 'happy' }) - await user.relateTo(post32, 'emoted', { emotion: 'cry' }) - variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } } - await expect(query({ query: postQueryFilteredByEmotions, variables })).resolves.toMatchObject( - { - data: { - Post: expect.arrayContaining(expected), - }, - }, - ) + await expect( + query({ query: postQueryFilteredByUsersFollowed, variables }), + ).resolves.toMatchObject(expected) }) }) }) @@ -656,7 +718,7 @@ describe('emotions', () => { const expected = { data: { AddPostEmotions: { - from: { id: 'u198' }, + from: { id: 'current-user' }, to: { id: 'p1376' }, emotion: 'happy', }, @@ -690,8 +752,8 @@ describe('emotions', () => { Post: [ { emotions: expect.arrayContaining([ - { emotion: 'happy', User: { id: 'u198' } }, - { emotion: 'surprised', User: { id: 'u198' } }, + { emotion: 'happy', User: { id: 'current-user' } }, + { emotion: 'surprised', User: { id: 'current-user' } }, ]), }, ], @@ -795,7 +857,7 @@ describe('emotions', () => { data: { RemovePostEmotions: { to: { id: 'p1376' }, - from: { id: 'u198' }, + from: { id: 'current-user' }, emotion: 'cry', }, }, diff --git a/backend/yarn.lock b/backend/yarn.lock index b77a99253..b1a1e0368 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3282,10 +3282,10 @@ escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@~6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.2.0.tgz#80e0b8714e3f6868c4ac2a25fbf39c02e73527a7" - integrity sha512-VLsgK/D+S/FEsda7Um1+N8FThec6LqE3vhcMyp8mlmto97y3fGf3DX7byJexGuOb1QY0Z/zz222U5t+xSfcZDQ== +eslint-config-prettier@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz#e73b48e59dc49d950843f3eb96d519e2248286a3" + integrity sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A== dependencies: get-stdin "^6.0.0" diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 32ed3ab92..d4b06fc7a 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -15,7 +15,8 @@ services: - ./webapp:/nitro-web - webapp_node_modules:/nitro-web/node_modules command: yarn run dev - user: root + environment: + - NUXT_BUILD=.nuxt-dist # avoid file ownership issues with shared folders factories: image: humanconnection/nitro-backend:builder build: diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 0c6576ca7..4a1acb96a 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -14,8 +14,6 @@ services: volumes: #/nitro-web - ./webapp/coverage:/nitro-web/coverage - environment: - - GRAPHQL_URI=http://backend:4000 backend: image: humanconnection/nitro-backend:builder build: diff --git a/docker-compose.yml b/docker-compose.yml index 3b147c631..45e2cdfdf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,6 @@ 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/webapp/Dockerfile b/webapp/Dockerfile index cf9c4c698..2fb807501 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -19,7 +19,7 @@ COPY . . FROM base as build-and-test RUN cp .env.template .env RUN yarn install --production=false --frozen-lockfile --non-interactive -RUN yarn run build +RUN NODE_ENV=production yarn run build FROM base as production ENV NODE_ENV=production diff --git a/webapp/components/FollowButton.vue b/webapp/components/FollowButton.vue index 7372b8a78..cc4662c85 100644 --- a/webapp/components/FollowButton.vue +++ b/webapp/components/FollowButton.vue @@ -59,16 +59,15 @@ export default { this.hovered = true } }, - toggle() { + async toggle() { const follow = !this.isFollowed const mutation = follow ? 'follow' : 'unfollow' this.hovered = false - this.$emit('optimistic', follow) - this.$apollo - .mutate({ + try { + await this.$apollo.mutate({ mutation: gql` mutation($id: ID!) { ${mutation}(id: $id, type: User) @@ -78,13 +77,11 @@ export default { id: this.followId, }, }) - .then(res => { - // this.$emit('optimistic', follow ? res.data.follow : follow) - this.$emit('update', follow) - }) - .catch(() => { - this.$emit('optimistic', !follow) - }) + + this.$emit('update', follow) + } catch { + this.$emit('optimistic', !follow) + } }, }, } diff --git a/webapp/components/User/User.vue b/webapp/components/User/User.vue index 080a4fcab..7c97c5755 100644 --- a/webapp/components/User/User.vue +++ b/webapp/components/User/User.vue @@ -71,8 +71,8 @@ @@ -131,6 +131,16 @@ export default { return name || this.$t('profile.userAnonym') }, }, + methods: { + optimisticFollow(follow) { + const inc = follow ? 1 : -1 + this.user.followedByCurrentUser = follow + this.user.followedByCount += inc + }, + updateFollow(follow) { + this.user.followedByCurrentUser = follow + }, + }, } diff --git a/webapp/package.json b/webapp/package.json index b633a0743..a5db37e72 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -10,11 +10,11 @@ "author": "Human Connection gGmbH", "private": false, "scripts": { - "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server", - "dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn dev", + "dev": "nuxt", + "dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn run dev", "storybook": "start-storybook -p 3002 -c storybook/", "build": "nuxt build", - "start": "cross-env node server/index.js", + "start": "nuxt start", "generate": "nuxt generate", "lint": "eslint --ext .js,.vue .", "precommit": "yarn lint", @@ -103,7 +103,7 @@ "core-js": "~2.6.9", "css-loader": "~3.2.0", "eslint": "~5.16.0", - "eslint-config-prettier": "~6.2.0", + "eslint-config-prettier": "~6.3.0", "eslint-config-standard": "~12.0.0", "eslint-loader": "~3.0.0", "eslint-plugin-import": "~2.18.2", @@ -118,7 +118,6 @@ "jest": "~24.9.0", "mutation-observer": "^1.0.3", "node-sass": "~4.12.0", - "nodemon": "~1.19.2", "prettier": "~1.18.2", "sass-loader": "~8.0.0", "style-loader": "~0.23.1", diff --git a/webapp/server/index.js b/webapp/server/index.js deleted file mode 100644 index d06bb1f6d..000000000 --- a/webapp/server/index.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express') -const consola = require('consola') -const { Nuxt, Builder } = require('nuxt') -const app = express() - -require('dotenv').config() - -const host = process.env.HOST || '127.0.0.1' -const port = process.env.PORT || 3000 -app.set('port', port) - -// Import and Set Nuxt.js options -let config = require('../nuxt.config.js') -config.dev = !(process.env.NODE_ENV === 'production') - -async function start() { - // Init Nuxt.js - const nuxt = new Nuxt(config) - - // Build only in dev mode - if (config.dev) { - const builder = new Builder(nuxt) - await builder.build() - } - - // Give nuxt middleware to express - app.use(nuxt.render) - - // Listen the server - app.listen(port, host) - consola.ready({ - message: `Server listening on http://${host}:${port}`, - badge: true, - }) -} -start() diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 3d754153b..859c392e4 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -6335,10 +6335,10 @@ escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^6.0.0, eslint-config-prettier@~6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.2.0.tgz#80e0b8714e3f6868c4ac2a25fbf39c02e73527a7" - integrity sha512-VLsgK/D+S/FEsda7Um1+N8FThec6LqE3vhcMyp8mlmto97y3fGf3DX7byJexGuOb1QY0Z/zz222U5t+xSfcZDQ== +eslint-config-prettier@^6.0.0, eslint-config-prettier@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz#e73b48e59dc49d950843f3eb96d519e2248286a3" + integrity sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A== dependencies: get-stdin "^6.0.0" @@ -10558,7 +10558,7 @@ node-sass@^4.12.0, node-sass@~4.12.0: stdout-stream "^1.4.0" "true-case-path" "^1.0.2" -nodemon@^1.19.1, nodemon@~1.19.2: +nodemon@^1.19.1: version "1.19.2" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.2.tgz#b0975147dc99b3761ceb595b3f9277084931dcc0" integrity sha512-hRLYaw5Ihyw9zK7NF+9EUzVyS6Cvgc14yh8CAYr38tPxJa6UrOxwAQ351GwrgoanHCF0FalQFn6w5eoX/LGdJw==