From 490fb7fdeb3f666756504f10fd073a9776b700d5 Mon Sep 17 00:00:00 2001 From: ogerly Date: Sat, 21 Dec 2019 22:11:38 +0100 Subject: [PATCH 001/107] first example dynamisch usermention for answer comments --- webapp/components/Comment/Comment.vue | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index cc7f815b9..3da39f435 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -55,6 +55,13 @@ + +
@@ -168,6 +175,22 @@ export default { this.$toast.error(err.message) } }, + answerComment() { + if (document.querySelector('.is-empty')) { + document.querySelector('.is-empty').innerHTML = '@' + this.comment.author.slug + } else { + const html = document.querySelector('.editor-content').innerHTML + const slug = + '@d' + + this.comment.author.slug + + '' + document.querySelector('.editor-content').innerHTML = html + '' + slug + } + }, }, } From 28839ee2b4f8c5d77965d24b4e24d312fc43eb91 Mon Sep 17 00:00:00 2001 From: ogerly Date: Mon, 6 Jan 2020 18:02:46 +0100 Subject: [PATCH 002/107] click on 'answered' to put the username of the respective comment into the editor as a mention. --- webapp/components/Comment/Comment.vue | 26 ++++++++++++++------------ webapp/locales/de.json | 3 ++- webapp/locales/en.json | 3 ++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index 3da39f435..46163bed0 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -56,7 +56,7 @@ @d' + + this.comment.author.slug + + '' + document.querySelector('.editor-content div').focus() if (document.querySelector('.is-empty')) { - document.querySelector('.is-empty').innerHTML = '@' + this.comment.author.slug + document.querySelector('.is-empty').innerHTML = slug + ' ' } else { - const html = document.querySelector('.editor-content').innerHTML - const slug = - '@d' + - this.comment.author.slug + - '' - document.querySelector('.editor-content').innerHTML = html + '' + slug + const html = document.querySelector('.editor-content').innerHTML + document.querySelector('.editor-content div').innerHTML = html + ' ' + slug + ' ' } }, }, diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 880d22c87..060892d03 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -269,7 +269,8 @@ "comment": { "submit": "Kommentiere", "submitted": "Kommentar Gesendet", - "updated": "Änderungen gespeichert" + "updated": "Änderungen gespeichert", + "answered": "answered" }, "edited": "bearbeitet" }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 94183d7e7..69215d484 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -423,7 +423,8 @@ "comment": { "submit": "Comment", "submitted": "Comment Submitted", - "updated": "Changes Saved" + "updated": "Changes Saved", + "answered": "answered" }, "edited": "edited" }, From 9aa680ce509f62f57774981815094a1c273d8fa6 Mon Sep 17 00:00:00 2001 From: ogerly Date: Tue, 7 Jan 2020 05:40:09 +0100 Subject: [PATCH 003/107] Fix lint --- webapp/components/Comment/Comment.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index 46163bed0..ed7b9b064 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -113,8 +113,8 @@ export default { if (this.isLongComment && this.isCollapsed) { return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH) } - - return this.comment.content + // console.log(this.comment.content.replace(/\?/gi, '?++')) + return this.comment.content.replace(/\?/gi, ' ') }, displaysComment() { return !this.unavailable || this.isModerator @@ -187,10 +187,10 @@ export default { '' document.querySelector('.editor-content div').focus() if (document.querySelector('.is-empty')) { - document.querySelector('.is-empty').innerHTML = slug + ' ' + document.querySelector('.is-empty').innerHTML = slug + ' ' } else { - const html = document.querySelector('.editor-content').innerHTML - document.querySelector('.editor-content div').innerHTML = html + ' ' + slug + ' ' + const html = document.querySelector('.editor-content').innerHTML + document.querySelector('.editor-content div').innerHTML = html + ' ' + slug + ' ' } }, }, From 223d1ca3c01659ebbf2c8da090cd17dfb5fc9f41 Mon Sep 17 00:00:00 2001 From: ogerly Date: Tue, 7 Jan 2020 06:02:19 +0100 Subject: [PATCH 004/107] remove console --- webapp/components/Comment/Comment.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index ed7b9b064..3063fc400 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -114,7 +114,7 @@ export default { return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH) } // console.log(this.comment.content.replace(/\?/gi, '?++')) - return this.comment.content.replace(/\?/gi, ' ') + return this.comment.content }, displaysComment() { return !this.unavailable || this.isModerator From 5d5574b1b5f7587139e3f822b62f388483379ab2 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 9 Jan 2020 16:07:25 +0100 Subject: [PATCH 005/107] Blocked users cannot comment on my posts --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/posts.js | 2 +- backend/src/schema/resolvers/users.js | 23 +++++++++ backend/src/schema/types/type/User.gql | 7 +-- webapp/components/Comment/Comment.vue | 6 +-- webapp/graphql/User.js | 7 +++ webapp/locales/en.json | 4 +- webapp/pages/post/_id/_slug/index.vue | 48 +++++++++++++++---- 8 files changed, 80 insertions(+), 18 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 3b42ae7fe..12a557232 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -102,6 +102,7 @@ export default shield( blockedUsers: isAuthenticated, notifications: isAuthenticated, Donations: isAuthenticated, + blockedByPostAuthor: isAuthenticated, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 47223faea..619b1de25 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -42,7 +42,7 @@ const maintainPinnedPosts = params => { export default { Query: { Post: async (object, params, context, resolveInfo) => { - params = await filterForBlockedUsers(params, context) + // params = await filterForBlockedUsers(params, context) params = await maintainPinnedPosts(params) return neo4jgraphql(object, params, context, resolveInfo) }, diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 0b3f13631..c2600ab24 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -48,6 +48,29 @@ export default { throw new UserInputError(e.message) } }, + blockedByPostAuthor: async (_parent, params, context, _resolveInfo) => { + const { postAuthorId } = params + const { user, driver } = context + const session = driver.session() + const readTxResultPromise = session.readTransaction(async transaction => { + const blockedByPostAuthorTransactionResponse = await transaction.run( + ` + MATCH (currentUser:User {id: $currentUserId})<-[relationship:BLOCKED]-(postAuthor:User {id: $postAuthorId}) + RETURN COUNT(relationship) >= 1 as blockedByPostAuthor + `, + { postAuthorId, currentUserId: user.id }, + ) + return blockedByPostAuthorTransactionResponse.records.map(record => + record.get('blockedByPostAuthor'), + ) + }) + try { + const [blockedByPostAuthor] = await readTxResultPromise + return blockedByPostAuthor + } finally { + session.close() + } + }, User: async (object, args, context, resolveInfo) => { const { email } = args if (email) { diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 243f45322..17ee2a733 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -68,10 +68,11 @@ type User { RETURN COUNT(u) >= 1 """ ) + isBlocked: Boolean! @cypher( statement: """ - MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 + MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId}) + RETURN COUNT(user) >= 1 """ ) @@ -159,7 +160,7 @@ type Query { orderBy: [_UserOrdering] filter: _UserFilter ): [User] - + blockedByPostAuthor(postAuthorId: ID!): Boolean! blockedUsers: [User] currentUser: User } diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index cc7f815b9..f776aa413 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -33,7 +33,7 @@
- '' }, diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 4ed832ad3..fa4b47423 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -216,3 +216,10 @@ export const checkSlugAvailableQuery = gql` } } ` +export const blockedByPostAuthor = () => { + return gql` + query($postAuthorId: ID!) { + blockedByPostAuthor(postAuthorId: $postAuthorId) + } + ` +} diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 6d8401e21..7d970998d 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -317,7 +317,9 @@ "their-perspective": "Vice versa: The blocked person will also no longer see your posts in their news feed.", "search": "Posts of blocked people disappear from your search results.", "notifications": "Blocked users will no longer receive notifications if they are mentioned in your posts.", - "closing": "This should be sufficient for now so that blocked users can no longer bother you." + "closing": "This should be sufficient for now so that blocked users can no longer bother you.", + "commenting-disabled": "Commenting is not possible at this time on this post.", + "commenting-explanation": "This can happen for several reasons, please see " }, "columns": { "name": "Name", diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index a94bf7b2d..bdfa13cf2 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -89,7 +89,19 @@ @toggleNewCommentForm="toggleNewCommentForm" /> - + + + + {{ $t('settings.blocked-users.explanation.commenting-disabled') }} +
+ {{ $t('settings.blocked-users.explanation.commenting-explanation') }} + https://human-connection.org +
+
@@ -102,12 +114,13 @@ import HcHashtag from '~/components/Hashtag/Hashtag' import ContentMenu from '~/components/ContentMenu/ContentMenu' import HcUser from '~/components/User/User' import HcShoutButton from '~/components/ShoutButton.vue' -import HcCommentForm from '~/components/CommentForm/CommentForm' +import CommentForm from '~/components/CommentForm/CommentForm' import HcCommentList from '~/components/CommentList/CommentList' import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers' import PostQuery from '~/graphql/PostQuery' import HcEmotions from '~/components/Emotions/Emotions' import PostMutations from '~/graphql/PostMutations' +import { blockedByPostAuthor } from '~/graphql/User' export default { name: 'PostSlug', @@ -121,7 +134,7 @@ export default { HcUser, HcShoutButton, ContentMenu, - HcCommentForm, + CommentForm, HcCommentList, HcEmotions, ContentViewer, @@ -138,15 +151,10 @@ export default { title: 'loading', showNewCommentForm: true, blurred: false, + blocked: null, + postAuthor: null, } }, - watch: { - Post(post) { - this.post = post[0] || {} - this.title = this.post.title - this.blurred = this.post.imageBlurred - }, - }, mounted() { setTimeout(() => { // NOTE: quick fix for jumping flexbox implementation @@ -215,6 +223,26 @@ export default { id: this.$route.params.id, } }, + update({ Post }) { + this.post = Post[0] || {} + this.title = this.post.title + this.blurred = this.post.imageBlurred + this.postAuthor = this.post.author + }, + fetchPolicy: 'cache-and-network', + }, + blockedByPostAuthor: { + query() { + return blockedByPostAuthor() + }, + variables() { + return { + postAuthorId: this.postAuthor ? this.postAuthor.id : this.$store.getters['auth/user'].id, + } + }, + update({ blockedByPostAuthor }) { + this.blocked = blockedByPostAuthor + }, fetchPolicy: 'cache-and-network', }, }, From 9d09dae2f44995a8216804afe15e4a51d4f8b5c8 Mon Sep 17 00:00:00 2001 From: ogerly Date: Tue, 14 Jan 2020 18:36:35 +0100 Subject: [PATCH 006/107] slug from editor comands set --- webapp/components/Comment/Comment.vue | 41 ++++++++----------- webapp/components/CommentForm/CommentForm.vue | 3 ++ webapp/components/CommentList/CommentList.vue | 4 ++ webapp/components/Editor/Editor.vue | 4 +- webapp/locales/de.json | 2 +- webapp/locales/en.json | 2 +- webapp/pages/post/_id/_slug/index.vue | 11 ++++- 7 files changed, 38 insertions(+), 29 deletions(-) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index 3063fc400..aeedb9e52 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -54,13 +54,14 @@
+ -
@@ -86,7 +87,6 @@ export default { isTarget, isCollapsed: !isTarget, openEditCommentMenu: false, - answered: this.$t('post.comment.answered'), } }, components: { @@ -113,7 +113,6 @@ export default { if (this.isLongComment && this.isCollapsed) { return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH) } - // console.log(this.comment.content.replace(/\?/gi, '?++')) return this.comment.content }, displaysComment() { @@ -149,6 +148,10 @@ export default { }, }, methods: { + reply(comment) { + const message = { slug: this.comment.author.slug, id: this.comment.author.id } + this.$emit('reply', message) + }, checkAnchor(anchor) { return `#${this.anchor}` === anchor }, @@ -176,23 +179,6 @@ export default { this.$toast.error(err.message) } }, - answerComment() { - const slug = - '@d' + - this.comment.author.slug + - '' - document.querySelector('.editor-content div').focus() - if (document.querySelector('.is-empty')) { - document.querySelector('.is-empty').innerHTML = slug + ' ' - } else { - const html = document.querySelector('.editor-content').innerHTML - document.querySelector('.editor-content div').innerHTML = html + ' ' + slug + ' ' - } - }, }, } @@ -218,6 +204,11 @@ export default { float: right; } +.answerbutton { + float: right; + top: 0px; +} + @keyframes highlight { 0% { border: 1px solid $color-primary; diff --git a/webapp/components/CommentForm/CommentForm.vue b/webapp/components/CommentForm/CommentForm.vue index 6cdd08af3..c2a0e4562 100644 --- a/webapp/components/CommentForm/CommentForm.vue +++ b/webapp/components/CommentForm/CommentForm.vue @@ -57,6 +57,9 @@ export default { } }, methods: { + reply(message) { + this.$refs.editor.insertReply(message) + }, updateEditorContent(value) { const sanitizedContent = this.$filters.removeHtml(value, false) if (!this.update) { diff --git a/webapp/components/CommentList/CommentList.vue b/webapp/components/CommentList/CommentList.vue index 25ed62f68..d21f2b407 100644 --- a/webapp/components/CommentList/CommentList.vue +++ b/webapp/components/CommentList/CommentList.vue @@ -8,6 +8,7 @@
{} }, }, methods: { + reply(message) { + this.$emit('reply', message) + }, checkAnchor(anchor) { return anchor === '#comments' }, diff --git a/webapp/components/Editor/Editor.vue b/webapp/components/Editor/Editor.vue index 6c8a1908a..f62d7abf7 100644 --- a/webapp/components/Editor/Editor.vue +++ b/webapp/components/Editor/Editor.vue @@ -141,7 +141,6 @@ export default { methods: { openSuggestionList({ items, query, range, command, virtualNode }, suggestionType) { this.suggestionType = suggestionType - this.query = this.sanitizeQuery(query) this.filteredItems = items this.suggestionRange = range @@ -237,6 +236,9 @@ export default { const content = e.getHTML() this.$emit('input', content) }, + insertReply(message) { + this.editor.commands.mention({ id: message.id, label: message.slug }) + }, toggleLinkInput(attrs, element) { if (!this.isLinkInputActive && attrs && element) { this.$refs.linkInput.linkUrl = attrs.href diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 060892d03..a394424cb 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -270,7 +270,7 @@ "submit": "Kommentiere", "submitted": "Kommentar Gesendet", "updated": "Änderungen gespeichert", - "answered": "answered" + "answer": "Antworten" }, "edited": "bearbeitet" }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 69215d484..e6e3426a7 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -424,7 +424,7 @@ "submit": "Comment", "submitted": "Comment Submitted", "updated": "Changes Saved", - "answered": "answered" + "answer": "Answer" }, "edited": "edited" }, diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index 067650d15..06bbe554d 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -72,12 +72,18 @@ - + @@ -150,6 +156,9 @@ export default { }, }, methods: { + reply(message) { + this.$refs.commentForm && this.$refs.commentForm.reply(message) + }, isAuthor(id) { return this.$store.getters['auth/user'].id === id }, From 8edb551c159272f005da502d7a021105dd0904d2 Mon Sep 17 00:00:00 2001 From: ogerly Date: Wed, 15 Jan 2020 07:40:01 +0100 Subject: [PATCH 007/107] ready to be merged or rebased --- webapp/components/Comment/Comment.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index aeedb9e52..b31414449 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -60,9 +60,9 @@ @click.prevent="reply" v-scroll-to="'.editor'" class="answerbutton" + size="small" > -
@@ -208,6 +208,9 @@ export default { float: right; top: 0px; } +.answerbutton:after { + clear: both; +} @keyframes highlight { 0% { From 6877c9da91c65037b06c653af2ea85effd827ebf Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 16 Jan 2020 12:29:28 +0100 Subject: [PATCH 008/107] Use new base-button, rename CSS class --- webapp/assets/_new/icons/svgs/level-down.svg | 5 +++++ webapp/components/Comment/Comment.vue | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100755 webapp/assets/_new/icons/svgs/level-down.svg diff --git a/webapp/assets/_new/icons/svgs/level-down.svg b/webapp/assets/_new/icons/svgs/level-down.svg new file mode 100755 index 000000000..e6455391e --- /dev/null +++ b/webapp/assets/_new/icons/svgs/level-down.svg @@ -0,0 +1,5 @@ + + +level-down + + diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index b31414449..b682c712a 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -54,14 +54,15 @@ - + > @@ -75,6 +76,7 @@ import ContentViewer from '~/components/Editor/ContentViewer' import HcCommentForm from '~/components/CommentForm/CommentForm' import CommentMutations from '~/graphql/CommentMutations' import scrollToAnchor from '~/mixins/scrollToAnchor.js' +import BaseButton from '~/components/_new/generic/BaseButton/BaseButton' export default { mixins: [scrollToAnchor], @@ -94,6 +96,7 @@ export default { ContentMenu, ContentViewer, HcCommentForm, + BaseButton, }, props: { routeHash: { type: String, default: () => '' }, @@ -204,11 +207,11 @@ export default { float: right; } -.answerbutton { +.reply-button { float: right; top: 0px; } -.answerbutton:after { +.reply-button:after { clear: both; } From c0e2675912b9c5cf1bdbc97ead9eab13f8c5aac7 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 13 Jan 2020 23:25:07 +0100 Subject: [PATCH 009/107] refactor(neo4j) Setup constraints with neode --- backend/README.md | 21 ++++++++++ backend/package.json | 4 +- backend/src/migration/migrate.js | 0 backend/src/migration/setup.js | 7 ++++ backend/src/models/Category.js | 2 +- backend/src/models/Post.js | 2 +- backend/src/models/UnverifiedEmailAddress.js | 2 +- backend/src/models/User.js | 2 +- neo4j/Dockerfile | 2 - neo4j/README.md | 9 ----- neo4j/db_setup.sh | 41 -------------------- 11 files changed, 35 insertions(+), 57 deletions(-) create mode 100644 backend/src/migration/migrate.js create mode 100644 backend/src/migration/setup.js delete mode 100755 neo4j/db_setup.sh diff --git a/backend/README.md b/backend/README.md index 14e6d0ddd..5474c30d7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -53,6 +53,27 @@ can issue GraphQL requests or access GraphQL Playground in the browser. ![GraphQL Playground](../.gitbook/assets/graphql-playground.png) +### Database Indices and Constraints + +Database indices and constraints need to be created when the database and the +backend is running: + +{% tabs %} +{% tab title="Docker" %} +```bash +docker-compose exec backend yarn run db:setup +``` +{% endtab %} + +{% tab title="Without Docker" %} +```bash +# in folder backend/ +# make sure your database is running on http://localhost:7474/browser/ +yarn run db:setup +``` +{% endtab %} +{% endtabs %} + #### Seed Database diff --git a/backend/package.json b/backend/package.json index cd8b3ea3f..acd0a9549 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,9 @@ "lint": "eslint src --config .eslintrc.js", "test": "jest --forceExit --detectOpenHandles --runInBand", "db:reset": "babel-node src/seed/reset-db.js", - "db:seed": "babel-node src/seed/seed-db.js" + "db:seed": "babel-node src/seed/seed-db.js", + "db:setup": "babel-node src/migration/setup.js", + "db:migrate": "babel-node src/migration/migrate.js" }, "author": "Human Connection gGmbH", "license": "MIT", diff --git a/backend/src/migration/migrate.js b/backend/src/migration/migrate.js new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/migration/setup.js b/backend/src/migration/setup.js new file mode 100644 index 000000000..33667ff1b --- /dev/null +++ b/backend/src/migration/setup.js @@ -0,0 +1,7 @@ +import { getNeode } from '../bootstrap/neo4j' + +(async() => { + await getNeode().schema.install() + console.log('Schema installed!') + process.exit(0) +})() diff --git a/backend/src/models/Category.js b/backend/src/models/Category.js index faf5f189f..223bb4f87 100644 --- a/backend/src/models/Category.js +++ b/backend/src/models/Category.js @@ -3,7 +3,7 @@ import uuid from 'uuid/v4' export default { id: { type: 'string', primary: true, default: uuid }, name: { type: 'string', required: true, default: false }, - slug: { type: 'string' }, + slug: { type: 'string', unique: 'true' }, icon: { type: 'string', required: true, default: false }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, updatedAt: { diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index fd1e5b2ac..c29036009 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -11,7 +11,7 @@ export default { direction: 'in', }, title: { type: 'string', disallow: [null], min: 3 }, - slug: { type: 'string', allow: [null] }, + slug: { type: 'string', allow: [null], unique: 'true', }, content: { type: 'string', disallow: [null], min: 3 }, contentExcerpt: { type: 'string', allow: [null] }, image: { type: 'string', allow: [null] }, diff --git a/backend/src/models/UnverifiedEmailAddress.js b/backend/src/models/UnverifiedEmailAddress.js index 489e8517a..c582ed011 100644 --- a/backend/src/models/UnverifiedEmailAddress.js +++ b/backend/src/models/UnverifiedEmailAddress.js @@ -1,5 +1,5 @@ export default { - email: { type: 'string', primary: true, lowercase: true, email: true }, + email: { type: 'string', lowercase: true, email: true }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, nonce: { type: 'string', token: true }, belongsTo: { diff --git a/backend/src/models/User.js b/backend/src/models/User.js index fc352dccc..049611eb7 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -4,7 +4,7 @@ export default { id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests actorId: { type: 'string', allow: [null] }, name: { type: 'string', disallow: [null], min: 3 }, - slug: { type: 'string', regex: /^[a-z0-9_-]+$/, lowercase: true }, + slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true }, encryptedPassword: 'string', avatar: { type: 'string', allow: [null] }, coverImg: { type: 'string', allow: [null] }, diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index 22dabe114..b068b22b2 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -4,7 +4,5 @@ LABEL Description="Neo4J database of the Social Network Human-Connection.org wit ARG BUILD_COMMIT ENV BUILD_COMMIT=$BUILD_COMMIT -COPY db_setup.sh /usr/local/bin/db_setup - RUN apt-get update && apt-get -y install wget htop RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.4/apoc-3.5.0.4-all.jar -P plugins/ diff --git a/neo4j/README.md b/neo4j/README.md index fe8825734..5df01cc71 100644 --- a/neo4j/README.md +++ b/neo4j/README.md @@ -18,15 +18,6 @@ docker-compose up You can access Neo4J through [http://localhost:7474/](http://localhost:7474/) for an interactive cypher shell and a visualization of the graph. -### Database Indices and Constraints - -Database indices and constraints need to be created when the database is -running. So start the container with the command above and run: - -```bash -docker-compose exec neo4j db_setup -``` - ## Installation without Docker diff --git a/neo4j/db_setup.sh b/neo4j/db_setup.sh deleted file mode 100755 index b7562d0c9..000000000 --- a/neo4j/db_setup.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -ENV_FILE=$(dirname "$0")/.env -[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" -if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then - echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." - echo "Setting up database constraints and indexes will probably fail because of authentication errors." - echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" -fi - -until echo 'RETURN "Connection successful" as info;' | cypher-shell -do - echo "Connecting to neo4j failed, trying again..." - sleep 1 -done - -echo ' -RETURN "Here is a list of indexes and constraints BEFORE THE SETUP:" as info; -CALL db.indexes(); -' | cypher-shell - -echo ' -CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"]); -CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"]); -CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE; -CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE; -CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE; -CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE; -CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE; - -CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; -CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; -CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; - -CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE; -' | cypher-shell - -echo ' -RETURN "Setting up all the indexes and constraints seems to have been successful. Here is a list AFTER THE SETUP:" as info; -CALL db.indexes(); -' | cypher-shell From 30268dec0943b4d8ad46418fca98bcb2fe75030b Mon Sep 17 00:00:00 2001 From: roschaefer Date: Tue, 14 Jan 2020 01:47:03 +0100 Subject: [PATCH 010/107] build(deps): Add `migrate` for neo4j data migrations Implement a migration to merge duplicate user accounts with reactive programming. Those duplicate user accounts existed, because around 40 users have decided to register again while we experienced a bug related to normalized emails in our database. --- backend/migrations/1579387929122-foo.js | 69 +++++++++++++++++ backend/package.json | 6 +- backend/src/db/migrationTemplate.js | 7 ++ .../src/{migration/setup.js => db/setup.sj} | 0 backend/src/migration/migrate.js | 0 backend/yarn.lock | 74 +++++++++++++++++-- 6 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/1579387929122-foo.js create mode 100644 backend/src/db/migrationTemplate.js rename backend/src/{migration/setup.js => db/setup.sj} (100%) delete mode 100644 backend/src/migration/migrate.js diff --git a/backend/migrations/1579387929122-foo.js b/backend/migrations/1579387929122-foo.js new file mode 100644 index 000000000..9d23051ac --- /dev/null +++ b/backend/migrations/1579387929122-foo.js @@ -0,0 +1,69 @@ +import { throwError, of, concat } from 'rxjs' +import { tap, flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators' +import CONFIG from '../src/config' +import { getNeode, getDriver } from '../src/bootstrap/neo4j' +import normalizeEmail from '../src/schema/resolvers//helpers/normalizeEmail' + + +export function up (next) { + const driver = getDriver() + const rxSession = driver.rxSession() + rxSession + .beginTransaction() + .pipe( + flatMap(txc => + concat( + txc + .run("MATCH (email:EmailAddress) RETURN email {.email}") + .records() + .pipe( + map(record => { + const { email } = record.get('email') + const normalizedEmail = normalizeEmail(email) + return { email, normalizedEmail } + }), + filter(({email, normalizedEmail}) => email !== normalizedEmail), + mergeMap(({email, normalizedEmail})=> { + return txc + .run(` + MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email}), (oldUser)-[previousRelationship]-(oldEmail) + MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email: $normalizedEmail}) + DELETE previousRelationship + WITH oldUser, oldEmail, user, email + CALL apoc.refactor.mergeNodes([user, oldUser], { properties: 'discard', mergeRels: true }) YIELD node as mergedUser + CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: 'discard', mergeRels: true }) YIELD node as mergedEmail + RETURN user {.*}, email {.*} + `, { email, normalizedEmail }) + .records() + .pipe( + map(r => ({ + oldEmail: email, + email: r.get('email'), + user: r.get('user'), + })) + ) + }), + ), + txc.commit(), + ).pipe(catchError(err => txc.rollback().pipe(throwError(err)))) + ) + ) + .subscribe({ + next: ({ user, email, oldUser, oldEmail }) => console.log(` + Merged: + ============================= + userId: ${user.id} + email: ${oldEmail} => ${email.email} + ============================= + `), + complete: () => { + console.log('Merging of duplicate users completed') + next() + }, + error: error => throw new Error(error) + }) +} + +export function down () { + throw new Error("Irreversible migration") +} diff --git a/backend/package.json b/backend/package.json index acd0a9549..4d23406c7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,8 +12,9 @@ "test": "jest --forceExit --detectOpenHandles --runInBand", "db:reset": "babel-node src/seed/reset-db.js", "db:seed": "babel-node src/seed/seed-db.js", - "db:setup": "babel-node src/migration/setup.js", - "db:migrate": "babel-node src/migration/migrate.js" + "db:setup": "babel-node src/db/setup.js", + "db:migrate:create": "migrate create --template-file./src/db/migrationTemplate.js", + "db:migrate": "migrate --compiler 'js:@babel/register'" }, "author": "Human Connection gGmbH", "license": "MIT", @@ -80,6 +81,7 @@ "metascraper-url": "^5.10.3", "metascraper-video": "^5.10.3", "metascraper-youtube": "^5.10.5", + "migrate": "^1.6.2", "minimatch": "^3.0.4", "mustache": "^4.0.0", "neo4j-driver": "^4.0.1", diff --git a/backend/src/db/migrationTemplate.js b/backend/src/db/migrationTemplate.js new file mode 100644 index 000000000..dff6564ef --- /dev/null +++ b/backend/src/db/migrationTemplate.js @@ -0,0 +1,7 @@ +export function up (next) { + next() +} + +export function down (next) { + next() +} diff --git a/backend/src/migration/setup.js b/backend/src/db/setup.sj similarity index 100% rename from backend/src/migration/setup.js rename to backend/src/db/setup.sj diff --git a/backend/src/migration/migrate.js b/backend/src/migration/migrate.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/yarn.lock b/backend/yarn.lock index 8c67f5a30..db1062af0 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1680,6 +1680,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + ansi-styles@^3.1.0, ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2492,6 +2497,17 @@ chalk@2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2704,6 +2720,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^2.9.0, commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" @@ -2714,11 +2735,6 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c" integrity sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA== -commander@~2.20.3: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3001,6 +3017,11 @@ date-fns@2.9.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2" integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA== +dateformat@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" + integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= + dayjs@^1.8.19: version "1.8.19" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.19.tgz#5117dc390d8f8e586d53891dbff3fa308f51abfe" @@ -3423,7 +3444,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -3834,7 +3855,6 @@ extsprintf@^1.2.0: faker@Marak/faker.js#master: version "4.1.0" - uid "3b2fa4aebccee52ae1bafc15d575061fb30c3cf1" resolved "https://codeload.github.com/Marak/faker.js/tar.gz/3b2fa4aebccee52ae1bafc15d575061fb30c3cf1" fast-deep-equal@^2.0.1: @@ -4387,6 +4407,13 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" @@ -6119,6 +6146,20 @@ micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +migrate@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/migrate/-/migrate-1.6.2.tgz#8970d596780553fe9f545bdf83806df8473f025b" + integrity sha512-XAFab+ArPTo9BHzmihKjsZ5THKRryenA+lwob0R+ax0hLDs7YzJFJT5YZE3gtntZgzdgcuFLs82EJFB/Dssr+g== + dependencies: + chalk "^1.1.3" + commander "^2.9.0" + dateformat "^2.0.0" + dotenv "^4.0.0" + inherits "^2.0.3" + minimatch "^3.0.3" + mkdirp "^0.5.1" + slug "^0.9.2" + mime-db@1.40.0: version "1.40.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" @@ -6163,7 +6204,7 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -minimatch@^3.0.4: +minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -7704,6 +7745,11 @@ serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -7784,6 +7830,13 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slug@^0.9.2: + version "0.9.4" + resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.4.tgz#fad5f1ef33150830c7688cd8500514576eccabd8" + integrity sha512-3YHq0TeJ4+AIFbJm+4UWSQs5A1mmeWOTQqydW3OoPmQfNKxlO96NDRTIrp+TBkmvEsEFrd+Z/LXw8OD/6OlZ5g== + dependencies: + unicode ">= 0.3.1" + slug@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slug/-/slug-2.1.0.tgz#293f8d53de7e55c15871846fd1bc36114841a8c7" @@ -8151,6 +8204,11 @@ supertest@~4.0.2: methods "^1.1.2" superagent "^3.8.3" +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + supports-color@^4.0.0: version "4.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" From daf2c40caec80587354cb1670a28b950afe181bf Mon Sep 17 00:00:00 2001 From: roschaefer Date: Sun, 19 Jan 2020 13:13:52 +0100 Subject: [PATCH 011/107] Implement Neo4J store for `migrate` --- backend/package.json | 8 ++- backend/src/db/migrate/store.js | 57 +++++++++++++++++++ backend/src/db/migrate/template.js | 7 +++ backend/src/db/migrationTemplate.js | 7 --- ...87929122-merge_duplicate_user_accounts.js} | 14 ++++- backend/src/db/{setup.sj => setup.js} | 0 backend/src/models/Post.js | 2 +- backend/yarn.lock | 2 +- 8 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 backend/src/db/migrate/store.js create mode 100644 backend/src/db/migrate/template.js delete mode 100644 backend/src/db/migrationTemplate.js rename backend/{migrations/1579387929122-foo.js => src/db/migrations/1579387929122-merge_duplicate_user_accounts.js} (77%) rename backend/src/db/{setup.sj => setup.js} (100%) diff --git a/backend/package.json b/backend/package.json index 4d23406c7..bbdf951ad 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,8 +13,10 @@ "db:reset": "babel-node src/seed/reset-db.js", "db:seed": "babel-node src/seed/seed-db.js", "db:setup": "babel-node src/db/setup.js", - "db:migrate:create": "migrate create --template-file./src/db/migrationTemplate.js", - "db:migrate": "migrate --compiler 'js:@babel/register'" + "__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations", + "db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js", + "db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js create", + "production:db:migrate": "migrate --migrations-dir ./dist/db/migrations --store ./dist/db/migrate/store.js" }, "author": "Human Connection gGmbH", "license": "MIT", @@ -106,7 +108,7 @@ "@babel/node": "~7.8.3", "@babel/plugin-proposal-throw-expressions": "^7.8.3", "@babel/preset-env": "~7.8.3", - "@babel/register": "~7.8.3", + "@babel/register": "^7.8.3", "apollo-server-testing": "~2.9.16", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.3", diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js new file mode 100644 index 000000000..68bf4e8d9 --- /dev/null +++ b/backend/src/db/migrate/store.js @@ -0,0 +1,57 @@ +import { getDriver } from '../../bootstrap/neo4j' + +class Store { + async load(fn) { + const driver = getDriver() + const session = driver.session() + const readTxResultPromise = session.readTransaction(async txc => { + const result = await txc.run( + 'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.timestamp DESC', + ) + return result.records.map(r => r.get('migration')) + }) + try { + const migrations = await readTxResultPromise + if (migrations.length <= 0) { + // eslint-disable-next-line no-console + console.log( + "No migrations found in database. If it's the first time you run migrations, then this is normal.", + ) + return fn(null, {}) + } + const [{ title: lastRun }] = migrations + fn(null, { lastRun, migrations }) + } catch (error) { + console.log(error) // eslint-disable-line no-console + } finally { + session.close() + } + } + + async save(set, fn) { + const driver = getDriver() + const session = driver.session() + const { migrations } = set + const writeTxResultPromise = session.writeTransaction(txc => { + return Promise.all( + migrations.map(migration => { + const { title, description, timestamp } = migration + const properties = { title, description, timestamp } + return txc.run('CREATE (migration:Migration) SET migration += $properties', { + properties, + }) + }), + ) + }) + try { + await writeTxResultPromise + } catch (error) { + console.log(error) // eslint-disable-line no-console + } finally { + session.close() + fn() + } + } +} + +module.exports = Store diff --git a/backend/src/db/migrate/template.js b/backend/src/db/migrate/template.js new file mode 100644 index 000000000..941f2a9e3 --- /dev/null +++ b/backend/src/db/migrate/template.js @@ -0,0 +1,7 @@ +export function up(next) { + next() +} + +export function down(next) { + next() +} diff --git a/backend/src/db/migrationTemplate.js b/backend/src/db/migrationTemplate.js deleted file mode 100644 index dff6564ef..000000000 --- a/backend/src/db/migrationTemplate.js +++ /dev/null @@ -1,7 +0,0 @@ -export function up (next) { - next() -} - -export function down (next) { - next() -} diff --git a/backend/migrations/1579387929122-foo.js b/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js similarity index 77% rename from backend/migrations/1579387929122-foo.js rename to backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js index 9d23051ac..7d2abcdeb 100644 --- a/backend/migrations/1579387929122-foo.js +++ b/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js @@ -1,10 +1,18 @@ import { throwError, of, concat } from 'rxjs' import { tap, flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators' -import CONFIG from '../src/config' -import { getNeode, getDriver } from '../src/bootstrap/neo4j' -import normalizeEmail from '../src/schema/resolvers//helpers/normalizeEmail' +import CONFIG from '../../src/config' +import { getNeode, getDriver } from '../../src/bootstrap/neo4j' +import normalizeEmail from '../../src/schema/resolvers//helpers/normalizeEmail' +export const description = ` + This migration merges duplicate :User and :EmailAddress nodes. It became + necessary after we implemented the email normalization but forgot to migrate + the existing data. Some (40) users decided to just register with a new account + but the same email address. On signup our backend would normalize the email, + which is good, but would also keep the existing unnormalized email address. + This led to about 40 duplicate user and email address nodes in our database. +` export function up (next) { const driver = getDriver() const rxSession = driver.rxSession() diff --git a/backend/src/db/setup.sj b/backend/src/db/setup.js similarity index 100% rename from backend/src/db/setup.sj rename to backend/src/db/setup.js diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index c29036009..e2e153a1b 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -11,7 +11,7 @@ export default { direction: 'in', }, title: { type: 'string', disallow: [null], min: 3 }, - slug: { type: 'string', allow: [null], unique: 'true', }, + slug: { type: 'string', allow: [null], unique: 'true' }, content: { type: 'string', disallow: [null], min: 3 }, contentExcerpt: { type: 'string', allow: [null] }, image: { type: 'string', allow: [null] }, diff --git a/backend/yarn.lock b/backend/yarn.lock index db1062af0..2c1b2efee 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -751,7 +751,7 @@ levenary "^1.1.0" semver "^5.5.0" -"@babel/register@^7.8.3", "@babel/register@~7.8.3": +"@babel/register@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.8.3.tgz#5d5d30cfcc918437535d724b8ac1e4a60c5db1f8" integrity sha512-t7UqebaWwo9nXWClIPLPloa5pN33A2leVs8Hf0e9g9YwUP8/H9NeR7DJU+4CXo23QtjChQv5a3DjEtT83ih1rg== From b063847849a84db885337dc8e84e75ddaf87011f Mon Sep 17 00:00:00 2001 From: roschaefer Date: Sun, 19 Jan 2020 23:53:36 +0100 Subject: [PATCH 012/107] refactor: Make `db:setup` init stage of `migrate` --- .travis.yml | 2 +- backend/README.md | 6 +- backend/package.json | 6 +- .../src/activitypub/routes/webfinger.spec.js | 4 +- backend/src/{seed/reset-db.js => db/clean.js} | 2 +- backend/src/db/migrate/store.js | 10 +- ...387929122-merge_duplicate_user_accounts.js | 89 ++++++------ backend/src/{bootstrap => db}/neo4j.js | 0 backend/src/{seed/seed-db.js => db/seed.js} | 4 +- backend/src/db/setup.js | 7 - backend/src/{seed => }/factories/badges.js | 0 .../src/{seed => }/factories/categories.js | 0 backend/src/{seed => }/factories/comments.js | 0 backend/src/{seed => }/factories/donations.js | 0 .../{seed => }/factories/emailAddresses.js | 0 backend/src/{seed => }/factories/index.js | 2 +- backend/src/{seed => }/factories/locations.js | 0 backend/src/{seed => }/factories/posts.js | 0 backend/src/{seed => }/factories/reports.js | 0 .../src/{seed => }/factories/socialMedia.js | 0 backend/src/{seed => }/factories/tags.js | 0 .../factories/unverifiedEmailAddresses.js | 0 backend/src/{seed => }/factories/users.js | 2 +- backend/src/jest/helpers.js | 5 - backend/src/jwt/decode.spec.js | 4 +- .../hashtags/hashtagsMiddleware.spec.js | 4 +- .../notificationsMiddleware.spec.js | 4 +- .../src/middleware/orderByMiddleware.spec.js | 4 +- .../src/middleware/permissionsMiddleware.js | 2 +- .../middleware/permissionsMiddleware.spec.js | 4 +- .../src/middleware/slugifyMiddleware.spec.js | 4 +- .../softDelete/softDeleteMiddleware.spec.js | 4 +- .../validation/validationMiddleware.spec.js | 4 +- backend/src/models/User.spec.js | 4 +- backend/src/schema/resolvers/comments.spec.js | 4 +- .../src/schema/resolvers/donations.spec.js | 4 +- backend/src/schema/resolvers/emails.spec.js | 4 +- backend/src/schema/resolvers/follow.js | 2 +- backend/src/schema/resolvers/follow.spec.js | 4 +- .../src/schema/resolvers/locations.spec.js | 4 +- .../src/schema/resolvers/moderation.spec.js | 4 +- .../schema/resolvers/notifications.spec.js | 4 +- .../schema/resolvers/passwordReset.spec.js | 4 +- backend/src/schema/resolvers/posts.spec.js | 4 +- backend/src/schema/resolvers/registration.js | 2 +- .../src/schema/resolvers/registration.spec.js | 4 +- backend/src/schema/resolvers/reports.spec.js | 4 +- backend/src/schema/resolvers/rewards.js | 2 +- backend/src/schema/resolvers/rewards.spec.js | 4 +- backend/src/schema/resolvers/shout.spec.js | 4 +- backend/src/schema/resolvers/socialMedia.js | 2 +- .../src/schema/resolvers/socialMedia.spec.js | 4 +- .../src/schema/resolvers/statistics.spec.js | 4 +- .../src/schema/resolvers/user_management.js | 2 +- .../schema/resolvers/user_management.spec.js | 4 +- backend/src/schema/resolvers/users.js | 2 +- backend/src/schema/resolvers/users.spec.js | 4 +- .../resolvers/users/blockedUsers.spec.js | 4 +- .../schema/resolvers/users/location.spec.js | 4 +- backend/src/seed/seed-helpers.js | 134 ------------------ backend/src/server.js | 2 +- backend/test/features/support/steps.js | 2 +- cypress/support/factories.js | 4 +- features/support/steps.js | 2 +- 64 files changed, 139 insertions(+), 270 deletions(-) rename backend/src/{seed/reset-db.js => db/clean.js} (91%) rename backend/src/{bootstrap => db}/neo4j.js (100%) rename backend/src/{seed/seed-db.js => db/seed.js} (99%) delete mode 100644 backend/src/db/setup.js rename backend/src/{seed => }/factories/badges.js (100%) rename backend/src/{seed => }/factories/categories.js (100%) rename backend/src/{seed => }/factories/comments.js (100%) rename backend/src/{seed => }/factories/donations.js (100%) rename backend/src/{seed => }/factories/emailAddresses.js (100%) rename backend/src/{seed => }/factories/index.js (96%) rename backend/src/{seed => }/factories/locations.js (100%) rename backend/src/{seed => }/factories/posts.js (100%) rename backend/src/{seed => }/factories/reports.js (100%) rename backend/src/{seed => }/factories/socialMedia.js (100%) rename backend/src/{seed => }/factories/tags.js (100%) rename backend/src/{seed => }/factories/unverifiedEmailAddresses.js (100%) rename backend/src/{seed => }/factories/users.js (95%) delete mode 100644 backend/src/jest/helpers.js delete mode 100644 backend/src/seed/seed-helpers.js diff --git a/.travis.yml b/.travis.yml index 19ba3ff9d..110655c79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ before_script: - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d - wait-on http://localhost:7474 - - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec neo4j db_setup + - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec backend yarn run db:migrate init script: - export CYPRESS_RETRIES=1 diff --git a/backend/README.md b/backend/README.md index 5474c30d7..c42c5ac85 100644 --- a/backend/README.md +++ b/backend/README.md @@ -61,7 +61,7 @@ backend is running: {% tabs %} {% tab title="Docker" %} ```bash -docker-compose exec backend yarn run db:setup +docker-compose exec backend yarn run db:migrate init ``` {% endtab %} @@ -69,7 +69,7 @@ docker-compose exec backend yarn run db:setup ```bash # in folder backend/ # make sure your database is running on http://localhost:7474/browser/ -yarn run db:setup +yarn run db:migrate init ``` {% endtab %} {% endtabs %} @@ -94,7 +94,7 @@ $ docker-compose exec backend yarn run db:reset # you could also wipe out your neo4j database and delete all volumes with: $ docker-compose down -v # if container is not running, run this command to set up your database indeces and contstraints -$ docker-compose run neo4j db_setup +$ docker-compose run backend yarn run db:migrate init ``` {% endtab %} diff --git a/backend/package.json b/backend/package.json index bbdf951ad..3c9fda287 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,9 +10,9 @@ "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql", "lint": "eslint src --config .eslintrc.js", "test": "jest --forceExit --detectOpenHandles --runInBand", - "db:reset": "babel-node src/seed/reset-db.js", - "db:seed": "babel-node src/seed/seed-db.js", - "db:setup": "babel-node src/db/setup.js", + "db:clean": "babel-node src/db/clean.js", + "db:reset": "yarn run db:clean", + "db:seed": "babel-node src/db/seed.js", "__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations", "db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js", "db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js create", diff --git a/backend/src/activitypub/routes/webfinger.spec.js b/backend/src/activitypub/routes/webfinger.spec.js index 4e9b2196d..06ca4577d 100644 --- a/backend/src/activitypub/routes/webfinger.spec.js +++ b/backend/src/activitypub/routes/webfinger.spec.js @@ -1,6 +1,6 @@ import { handler } from './webfinger' -import Factory from '../../seed/factories' -import { getDriver } from '../../bootstrap/neo4j' +import Factory from '../../factories' +import { getDriver } from '../../db/neo4j' let resource, res, json, status, contentType diff --git a/backend/src/seed/reset-db.js b/backend/src/db/clean.js similarity index 91% rename from backend/src/seed/reset-db.js rename to backend/src/db/clean.js index 125d135d8..cbb1412e2 100644 --- a/backend/src/seed/reset-db.js +++ b/backend/src/db/clean.js @@ -1,4 +1,4 @@ -import { cleanDatabase } from './factories' +import { cleanDatabase } from '../factories' if (process.env.NODE_ENV === 'production') { throw new Error(`You cannot clean the database in production environment!`) diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 68bf4e8d9..9984b3971 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -1,6 +1,14 @@ -import { getDriver } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../db/neo4j' class Store { + async init(fn) { + const neode = getNeode() + await getNeode().schema.install() + // eslint-disable-next-line no-console + console.log('Successfully created database indices and constraints!') + neode.driver.close() + } + async load(fn) { const driver = getDriver() const session = driver.session() diff --git a/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js b/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js index 7d2abcdeb..9dbc8ad05 100644 --- a/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js +++ b/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js @@ -1,8 +1,7 @@ -import { throwError, of, concat } from 'rxjs' -import { tap, flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators' -import CONFIG from '../../src/config' -import { getNeode, getDriver } from '../../src/bootstrap/neo4j' -import normalizeEmail from '../../src/schema/resolvers//helpers/normalizeEmail' +import { throwError, concat } from 'rxjs' +import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators' +import { getDriver } from '../neo4j' +import normalizeEmail from '../../schema/resolvers//helpers/normalizeEmail' export const description = ` This migration merges duplicate :User and :EmailAddress nodes. It became @@ -13,7 +12,7 @@ export const description = ` This led to about 40 duplicate user and email address nodes in our database. ` -export function up (next) { +export function up(next) { const driver = getDriver() const rxSession = driver.rxSession() rxSession @@ -22,18 +21,19 @@ export function up (next) { flatMap(txc => concat( txc - .run("MATCH (email:EmailAddress) RETURN email {.email}") - .records() - .pipe( - map(record => { - const { email } = record.get('email') - const normalizedEmail = normalizeEmail(email) - return { email, normalizedEmail } - }), - filter(({email, normalizedEmail}) => email !== normalizedEmail), - mergeMap(({email, normalizedEmail})=> { - return txc - .run(` + .run('MATCH (email:EmailAddress) RETURN email {.email}') + .records() + .pipe( + map(record => { + const { email } = record.get('email') + const normalizedEmail = normalizeEmail(email) + return { email, normalizedEmail } + }), + filter(({ email, normalizedEmail }) => email !== normalizedEmail), + mergeMap(({ email, normalizedEmail }) => { + return txc + .run( + ` MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email}), (oldUser)-[previousRelationship]-(oldEmail) MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email: $normalizedEmail}) DELETE previousRelationship @@ -41,37 +41,44 @@ export function up (next) { CALL apoc.refactor.mergeNodes([user, oldUser], { properties: 'discard', mergeRels: true }) YIELD node as mergedUser CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: 'discard', mergeRels: true }) YIELD node as mergedEmail RETURN user {.*}, email {.*} - `, { email, normalizedEmail }) - .records() - .pipe( - map(r => ({ - oldEmail: email, - email: r.get('email'), - user: r.get('user'), - })) - ) - }), - ), + `, + { email, normalizedEmail }, + ) + .records() + .pipe( + map(r => ({ + oldEmail: email, + email: r.get('email'), + user: r.get('user'), + })), + ) + }), + ), txc.commit(), - ).pipe(catchError(err => txc.rollback().pipe(throwError(err)))) - ) + ).pipe(catchError(err => txc.rollback().pipe(throwError(err)))), + ), ) .subscribe({ - next: ({ user, email, oldUser, oldEmail }) => console.log(` - Merged: - ============================= - userId: ${user.id} - email: ${oldEmail} => ${email.email} - ============================= - `), + next: ({ user, email, oldUser, oldEmail }) => + // eslint-disable-next-line no-console + console.log(` + Merged: + ============================= + userId: ${user.id} + email: ${oldEmail} => ${email.email} + ============================= + `), complete: () => { + // eslint-disable-next-line no-console console.log('Merging of duplicate users completed') next() }, - error: error => throw new Error(error) + error: error => { + throw new Error(error) + }, }) } -export function down () { - throw new Error("Irreversible migration") +export function down() { + throw new Error('Irreversible migration') } diff --git a/backend/src/bootstrap/neo4j.js b/backend/src/db/neo4j.js similarity index 100% rename from backend/src/bootstrap/neo4j.js rename to backend/src/db/neo4j.js diff --git a/backend/src/seed/seed-db.js b/backend/src/db/seed.js similarity index 99% rename from backend/src/seed/seed-db.js rename to backend/src/db/seed.js index 4178169bb..f3c46b0d4 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/db/seed.js @@ -2,8 +2,8 @@ import faker from 'faker' import sample from 'lodash/sample' import { createTestClient } from 'apollo-server-testing' import createServer from '../server' -import Factory from './factories' -import { getNeode, getDriver } from '../bootstrap/neo4j' +import Factory from '../factories' +import { getNeode, getDriver } from '../db/neo4j' import { gql } from '../helpers/jest' const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] diff --git a/backend/src/db/setup.js b/backend/src/db/setup.js deleted file mode 100644 index 33667ff1b..000000000 --- a/backend/src/db/setup.js +++ /dev/null @@ -1,7 +0,0 @@ -import { getNeode } from '../bootstrap/neo4j' - -(async() => { - await getNeode().schema.install() - console.log('Schema installed!') - process.exit(0) -})() diff --git a/backend/src/seed/factories/badges.js b/backend/src/factories/badges.js similarity index 100% rename from backend/src/seed/factories/badges.js rename to backend/src/factories/badges.js diff --git a/backend/src/seed/factories/categories.js b/backend/src/factories/categories.js similarity index 100% rename from backend/src/seed/factories/categories.js rename to backend/src/factories/categories.js diff --git a/backend/src/seed/factories/comments.js b/backend/src/factories/comments.js similarity index 100% rename from backend/src/seed/factories/comments.js rename to backend/src/factories/comments.js diff --git a/backend/src/seed/factories/donations.js b/backend/src/factories/donations.js similarity index 100% rename from backend/src/seed/factories/donations.js rename to backend/src/factories/donations.js diff --git a/backend/src/seed/factories/emailAddresses.js b/backend/src/factories/emailAddresses.js similarity index 100% rename from backend/src/seed/factories/emailAddresses.js rename to backend/src/factories/emailAddresses.js diff --git a/backend/src/seed/factories/index.js b/backend/src/factories/index.js similarity index 96% rename from backend/src/seed/factories/index.js rename to backend/src/factories/index.js index ff6a52a76..c3ab14f64 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/factories/index.js @@ -1,4 +1,4 @@ -import { getDriver, getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../db/neo4j' const factories = { Badge: require('./badges.js').default, diff --git a/backend/src/seed/factories/locations.js b/backend/src/factories/locations.js similarity index 100% rename from backend/src/seed/factories/locations.js rename to backend/src/factories/locations.js diff --git a/backend/src/seed/factories/posts.js b/backend/src/factories/posts.js similarity index 100% rename from backend/src/seed/factories/posts.js rename to backend/src/factories/posts.js diff --git a/backend/src/seed/factories/reports.js b/backend/src/factories/reports.js similarity index 100% rename from backend/src/seed/factories/reports.js rename to backend/src/factories/reports.js diff --git a/backend/src/seed/factories/socialMedia.js b/backend/src/factories/socialMedia.js similarity index 100% rename from backend/src/seed/factories/socialMedia.js rename to backend/src/factories/socialMedia.js diff --git a/backend/src/seed/factories/tags.js b/backend/src/factories/tags.js similarity index 100% rename from backend/src/seed/factories/tags.js rename to backend/src/factories/tags.js diff --git a/backend/src/seed/factories/unverifiedEmailAddresses.js b/backend/src/factories/unverifiedEmailAddresses.js similarity index 100% rename from backend/src/seed/factories/unverifiedEmailAddresses.js rename to backend/src/factories/unverifiedEmailAddresses.js diff --git a/backend/src/seed/factories/users.js b/backend/src/factories/users.js similarity index 95% rename from backend/src/seed/factories/users.js rename to backend/src/factories/users.js index d56c42d0a..57f69b76b 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/factories/users.js @@ -1,6 +1,6 @@ import faker from 'faker' import uuid from 'uuid/v4' -import encryptPassword from '../../helpers/encryptPassword' +import encryptPassword from '../helpers/encryptPassword' import slugify from 'slug' export default function create() { diff --git a/backend/src/jest/helpers.js b/backend/src/jest/helpers.js deleted file mode 100644 index 201d68c14..000000000 --- a/backend/src/jest/helpers.js +++ /dev/null @@ -1,5 +0,0 @@ -//* This is a fake ES2015 template string, just to benefit of syntax -// highlighting of `gql` template strings in certain editors. -export function gql(strings) { - return strings.join('') -} diff --git a/backend/src/jwt/decode.spec.js b/backend/src/jwt/decode.spec.js index 7aa703d97..71444a3e5 100644 --- a/backend/src/jwt/decode.spec.js +++ b/backend/src/jwt/decode.spec.js @@ -1,5 +1,5 @@ -import Factory from '../seed/factories/index' -import { getDriver, getNeode } from '../bootstrap/neo4j' +import Factory from '../factories/index' +import { getDriver, getNeode } from '../db/neo4j' import decode from './decode' const factory = Factory() diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js index 0fa1e2dc5..2247e692d 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js @@ -1,7 +1,7 @@ import { gql } from '../../helpers/jest' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { createTestClient } from 'apollo-server-testing' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' let server diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index c5f5990d3..136388b88 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -1,7 +1,7 @@ import { gql } from '../../helpers/jest' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { createTestClient } from 'apollo-server-testing' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' let server, query, mutate, notifiedUser, authenticatedUser diff --git a/backend/src/middleware/orderByMiddleware.spec.js b/backend/src/middleware/orderByMiddleware.spec.js index 129f3a8b4..8d92a5b5d 100644 --- a/backend/src/middleware/orderByMiddleware.spec.js +++ b/backend/src/middleware/orderByMiddleware.spec.js @@ -1,6 +1,6 @@ import { gql } from '../helpers/jest' -import Factory from '../seed/factories' -import { getNeode, getDriver } from '../bootstrap/neo4j' +import Factory from '../factories' +import { getNeode, getDriver } from '../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../server' diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index a4c41871f..3e5bbd6e9 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,5 +1,5 @@ import { rule, shield, deny, allow, or } from 'graphql-shield' -import { getNeode } from '../bootstrap/neo4j' +import { getNeode } from '../db/neo4j' import CONFIG from '../config' const debug = !!CONFIG.DEBUG diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index 60aff961d..a4f13ea0c 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -1,8 +1,8 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../server' -import Factory from '../seed/factories' +import Factory from '../factories' import { gql } from '../helpers/jest' -import { getDriver, getNeode } from '../bootstrap/neo4j' +import { getDriver, getNeode } from '../db/neo4j' const factory = Factory() const instance = getNeode() diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 1c2e59317..cf9f0941c 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -1,6 +1,6 @@ -import Factory from '../seed/factories' +import Factory from '../factories' import { gql } from '../helpers/jest' -import { getNeode, getDriver } from '../bootstrap/neo4j' +import { getNeode, getDriver } from '../db/neo4j' import createServer from '../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index b7c16dfd3..6e1735af2 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js index d093f939a..38cd010b4 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.js +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -1,6 +1,6 @@ import { gql } from '../../helpers/jest' -import Factory from '../../seed/factories' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import Factory from '../../factories' +import { getNeode, getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js index 433cc5a6f..7bdde7014 100644 --- a/backend/src/models/User.spec.js +++ b/backend/src/models/User.spec.js @@ -1,5 +1,5 @@ -import Factory from '../seed/factories' -import { getNeode } from '../bootstrap/neo4j' +import Factory from '../factories' +import { getNeode } from '../db/neo4j' const factory = Factory() const neode = getNeode() diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index f96a60514..9877161db 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -1,8 +1,8 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index d8dd5db06..c382eb475 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' let mutate, query, authenticatedUser, variables diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 82ce43337..97a1f0c29 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getDriver, getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/follow.js b/backend/src/schema/resolvers/follow.js index 0416fe3d2..80cce8400 100644 --- a/backend/src/schema/resolvers/follow.js +++ b/backend/src/schema/resolvers/follow.js @@ -1,4 +1,4 @@ -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' const neode = getNeode() diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index ff884666e..ad836a461 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' -import { getDriver, getNeode } from '../../bootstrap/neo4j' +import Factory from '../../factories' +import { getDriver, getNeode } from '../../db/neo4j' import createServer from '../../server' import { gql } from '../../helpers/jest' diff --git a/backend/src/schema/resolvers/locations.spec.js b/backend/src/schema/resolvers/locations.spec.js index f4a846afd..aba11f9bc 100644 --- a/backend/src/schema/resolvers/locations.spec.js +++ b/backend/src/schema/resolvers/locations.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index f76cbdf46..cd502be75 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' const factory = Factory() diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 89bbd2528..a5c46e930 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getDriver } from '../../bootstrap/neo4j' +import { getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index be3c8c085..d7b3a0157 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createPasswordReset from './helpers/createPasswordReset' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index dcbd16d5d..71d1aa359 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' const driver = getDriver() diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index e03f294cd..1e7708395 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -1,5 +1,5 @@ import { UserInputError } from 'apollo-server' -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' import fileUpload from './fileUpload' import encryptPassword from '../../helpers/encryptPassword' import generateNonce from './helpers/generateNonce' diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 8f3a7ac39..23b1f9d2a 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getDriver, getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 8b1bb925d..7f827b111 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,8 +1,8 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getDriver, getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../db/neo4j' const factory = Factory() const instance = getNeode() diff --git a/backend/src/schema/resolvers/rewards.js b/backend/src/schema/resolvers/rewards.js index 44bdab770..311cfd2e6 100644 --- a/backend/src/schema/resolvers/rewards.js +++ b/backend/src/schema/resolvers/rewards.js @@ -1,4 +1,4 @@ -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' import { UserInputError } from 'apollo-server' const neode = getNeode() diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index e6f67ecab..fe2807f25 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' const factory = Factory() diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index e747946aa..104a28399 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' let mutate, query, authenticatedUser, variables diff --git a/backend/src/schema/resolvers/socialMedia.js b/backend/src/schema/resolvers/socialMedia.js index c206778e5..c5b9dcd91 100644 --- a/backend/src/schema/resolvers/socialMedia.js +++ b/backend/src/schema/resolvers/socialMedia.js @@ -1,4 +1,4 @@ -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' import Resolver from './helpers/Resolver' const neode = getNeode() diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index 8f6d91d43..f292b58a0 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -1,8 +1,8 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' const driver = getDriver() const factory = Factory() diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js index 48baf00cd..e2b9dafe4 100644 --- a/backend/src/schema/resolvers/statistics.spec.js +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' let query, authenticatedUser diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index 4d035d9fa..4d40a6f63 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -1,7 +1,7 @@ import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' import normalizeEmail from './helpers/normalizeEmail' import log from './helpers/databaseLogger' diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index 3527e5dc2..5e7043e74 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -1,11 +1,11 @@ import jwt from 'jsonwebtoken' import CONFIG from './../../config' -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' import { createTestClient } from 'apollo-server-testing' import createServer, { context } from '../../server' import encode from '../../jwt/encode' -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' const factory = Factory() const neode = getNeode() diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 0b3f13631..6183511f1 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -1,6 +1,6 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' -import { getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../db/neo4j' import { UserInputError, ForbiddenError } from 'apollo-server' import Resolver from './helpers/Resolver' import log from './helpers/databaseLogger' diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 5d1ebd8e2..cfd84fcf7 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,6 +1,6 @@ -import Factory from '../../seed/factories' +import Factory from '../../factories' import { gql } from '../../helpers/jest' -import { getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/users/blockedUsers.spec.js b/backend/src/schema/resolvers/users/blockedUsers.spec.js index 11bcb823d..bec8b59d0 100644 --- a/backend/src/schema/resolvers/users/blockedUsers.spec.js +++ b/backend/src/schema/resolvers/users/blockedUsers.spec.js @@ -1,8 +1,8 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../../../server' -import Factory from '../../../seed/factories' +import Factory from '../../../factories' import { gql } from '../../../helpers/jest' -import { getNeode, getDriver } from '../../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../../db/neo4j' const driver = getDriver() const factory = Factory() diff --git a/backend/src/schema/resolvers/users/location.spec.js b/backend/src/schema/resolvers/users/location.spec.js index 59d093afb..f7315174c 100644 --- a/backend/src/schema/resolvers/users/location.spec.js +++ b/backend/src/schema/resolvers/users/location.spec.js @@ -1,6 +1,6 @@ import { gql } from '../../../helpers/jest' -import Factory from '../../../seed/factories' -import { getNeode, getDriver } from '../../../bootstrap/neo4j' +import Factory from '../../../factories' +import { getNeode, getDriver } from '../../../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../../../server' diff --git a/backend/src/seed/seed-helpers.js b/backend/src/seed/seed-helpers.js deleted file mode 100644 index 913ca1d54..000000000 --- a/backend/src/seed/seed-helpers.js +++ /dev/null @@ -1,134 +0,0 @@ -const _ = require('lodash') -const faker = require('faker') -const unsplashTopics = [ - 'love', - 'family', - 'spring', - 'business', - 'nature', - 'travel', - 'happy', - 'landscape', - 'health', - 'friends', - 'computer', - 'autumn', - 'space', - 'animal', - 'smile', - 'face', - 'people', - 'portrait', - 'amazing', -] -let unsplashTopicsTmp = [] - -const ngoLogos = [ - 'http://www.fetchlogos.com/wp-content/uploads/2015/11/Girl-Scouts-Of-The-Usa-Logo.jpg', - 'http://logos.textgiraffe.com/logos/logo-name/Ngo-designstyle-friday-m.png', - 'http://seeklogo.com/images/N/ngo-logo-BD53A3E024-seeklogo.com.png', - 'https://dcassetcdn.com/design_img/10133/25833/25833_303600_10133_image.jpg', - 'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/20.jpg', - 'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/33.jpg', - null, -] - -const difficulties = ['easy', 'medium', 'hard'] - -export default { - randomItem: (items, filter) => { - const ids = filter - ? Object.keys(items).filter(id => { - return filter(items[id]) - }) - : _.keys(items) - const randomIds = _.shuffle(ids) - return items[randomIds.pop()] - }, - randomItems: (items, key = 'id', min = 1, max = 1) => { - const randomIds = _.shuffle(_.keys(items)) - const res = [] - - const count = _.random(min, max) - - for (let i = 0; i < count; i++) { - let r = items[randomIds.pop()][key] - if (key === 'id') { - r = r.toString() - } - res.push(r) - } - return res - }, - random: items => { - return _.shuffle(items).pop() - }, - randomDifficulty: () => { - return _.shuffle(difficulties).pop() - }, - randomLogo: () => { - return _.shuffle(ngoLogos).pop() - }, - randomUnsplashUrl: () => { - if (Math.random() < 0.6) { - // do not attach images in 60 percent of the cases (faster seeding) - return - } - if (unsplashTopicsTmp.length < 2) { - unsplashTopicsTmp = _.shuffle(unsplashTopics) - } - return ( - 'https://source.unsplash.com/daily?' + unsplashTopicsTmp.pop() + ',' + unsplashTopicsTmp.pop() - ) - }, - randomCategories: (seederstore, allowEmpty = false) => { - let count = Math.round(Math.random() * 3) - if (allowEmpty === false && count === 0) { - count = 1 - } - const categorieIds = _.shuffle(_.keys(seederstore.categories)) - const ids = [] - for (let i = 0; i < count; i++) { - ids.push(categorieIds.pop()) - } - return ids - }, - randomAddresses: () => { - const count = Math.round(Math.random() * 3) - const addresses = [] - for (let i = 0; i < count; i++) { - addresses.push({ - city: faker.address.city(), - zipCode: faker.address.zipCode(), - street: faker.address.streetAddress(), - country: faker.address.countryCode(), - lat: 54.032726 - Math.random() * 10, - lng: 6.558838 + Math.random() * 10, - }) - } - return addresses - }, - /** - * Get array of ids from the given seederstore items after mapping them by the key in the values - * - * @param items items from the seederstore - * @param values values for which you need the ids - * @param key the field key that is represented in the values (slug, name, etc.) - */ - mapIdsByKey: (items, values, key) => { - const res = [] - values.forEach(value => { - res.push(_.find(items, [key, value]).id.toString()) - }) - return res - }, - genInviteCode: () => { - const chars = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ' - let code = '' - for (let i = 0; i < 8; i++) { - const n = _.random(0, chars.length - 1) - code += chars.substr(n, 1) - } - return code - }, -} diff --git a/backend/src/server.js b/backend/src/server.js index bd9973a39..02e166b71 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -3,7 +3,7 @@ import helmet from 'helmet' import { ApolloServer } from 'apollo-server-express' import CONFIG from './config' import middleware from './middleware' -import { getNeode, getDriver } from './bootstrap/neo4j' +import { getNeode, getDriver } from './db/neo4j' import decode from './jwt/decode' import schema from './schema' import webfinger from './activitypub/routes/webfinger' diff --git a/backend/test/features/support/steps.js b/backend/test/features/support/steps.js index 73d059348..70802f4e2 100644 --- a/backend/test/features/support/steps.js +++ b/backend/test/features/support/steps.js @@ -3,7 +3,7 @@ import { Given, When, Then, AfterAll } from 'cucumber' import { expect } from 'chai' // import { client } from '../../../src/activitypub/apollo-client' import { GraphQLClient } from 'graphql-request' -import Factory from '../../../src/seed/factories' +import Factory from '../../../src/factories' const debug = require('debug')('ea:test:steps') const factory = Factory() diff --git a/cypress/support/factories.js b/cypress/support/factories.js index e0b6210d8..1b76a1a01 100644 --- a/cypress/support/factories.js +++ b/cypress/support/factories.js @@ -1,5 +1,5 @@ -import Factory from '../../backend/src/seed/factories' -import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j' +import Factory from '../../backend/src/factories' +import { getDriver, getNeode } from '../../backend/src/db/neo4j' const neo4jConfigs = { uri: Cypress.env('NEO4J_URI'), diff --git a/features/support/steps.js b/features/support/steps.js index 923dc9766..71f493834 100644 --- a/features/support/steps.js +++ b/features/support/steps.js @@ -1,6 +1,6 @@ // features/support/steps.js import { Given, When, Then, After, AfterAll } from 'cucumber' -import Factory from '../../backend/src/seed/factories' +import Factory from '../../backend/src/factories' import dotenv from 'dotenv' import expect from 'expect' From ce664040c60dfb0fa4d84cb457c3be74ddcf58b5 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Sun, 19 Jan 2020 17:51:27 +0100 Subject: [PATCH 013/107] docs(backend): How to create & run data migrations --- backend/README.md | 32 +++++++++++ neo4j/README.md | 14 ----- ...ge_disabled_relationship_to_report_node.sh | 55 ------------------- .../db_manipulation/add_image_aspect_ratio.sh | 22 -------- ...ge_disabled_relationship_to_report_node.sh | 51 ----------------- 5 files changed, 32 insertions(+), 142 deletions(-) delete mode 100755 neo4j/change_disabled_relationship_to_report_node.sh delete mode 100755 neo4j/db_manipulation/add_image_aspect_ratio.sh delete mode 100755 neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh diff --git a/backend/README.md b/backend/README.md index c42c5ac85..7fd49faf8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -111,6 +111,38 @@ $ yarn run db:reset {% endtab %} {% endtabs %} +### Data migrations + +Although Neo4J is schema-less,you might find yourself in a situation in which +you have to migrate your data e.g. because your data modeling has changed. + +{% tabs %} +{% tab title="Docker" %} +Generate a data migration file: +```bash +$ docker-compose exec backend yarn run db:migrate:create your_data_migration +# Edit the file in ./src/db/migrations/ +``` + +To run the migration: +```bash +$ docker-compose exec backend yarn run db:migrate up +``` +{% endtab %} +{% tab title="Without Docker" %} +Generate a data migration file: +```bash +$ yarn run db:migrate:create your_data_migration +# Edit the file in ./src/db/migrations/ +``` + +To run the migration: +```bash +$ yarn run db:migrate up +``` +{% endtab %} +{% endtabs %} + # Testing **Beware**: We have no multiple database setup at the moment. We clean the diff --git a/neo4j/README.md b/neo4j/README.md index 5df01cc71..a4242b512 100644 --- a/neo4j/README.md +++ b/neo4j/README.md @@ -36,20 +36,6 @@ Then make sure to allow Apoc procedures by adding the following line to your Neo ``` dbms.security.procedures.unrestricted=apoc.* ``` -### Database Indices and Constraints - -If you have `cypher-shell` available with your local installation of neo4j you -can run: - -```bash -# in folder neo4j/ -$ cp .env.template .env -$ ./db_setup.sh -``` - -Otherwise, if you don't have `cypher-shell` available, copy the cypher -statements [from the `db_setup.sh` script](https://github.com/Human-Connection/Human-Connection/blob/master/neo4j/db_setup.sh) and paste the scripts into your -[database browser frontend](http://localhost:7474). ### Alternatives diff --git a/neo4j/change_disabled_relationship_to_report_node.sh b/neo4j/change_disabled_relationship_to_report_node.sh deleted file mode 100755 index 2f44b8e59..000000000 --- a/neo4j/change_disabled_relationship_to_report_node.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -ENV_FILE=$(dirname "$0")/.env -[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" - -if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then - echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." - echo "Database manipulation is not possible without connecting to the database." - echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" -fi - -until echo 'RETURN "Connection successful" as info;' | cypher-shell -do - echo "Connecting to neo4j failed, trying again..." - sleep 1 -done - -echo " -// convert old DISABLED to new REVIEWED-Report-BELONGS_TO structure -MATCH (moderator:User)-[disabled:DISABLED]->(disabledResource) -WHERE disabledResource:User OR disabledResource:Comment OR disabledResource:Post -DELETE disabled -CREATE (moderator)-[review:REVIEWED]->(report:Report)-[:BELONGS_TO]->(disabledResource) -SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true -SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false - -// if disabledResource has no filed report, then create a moderators default filed report -WITH moderator, disabledResource, report -OPTIONAL MATCH (disabledResourceReporter:User)-[existingFiledReport:FILED]->(disabledResource) -FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NULL THEN [1] ELSE [] END | - CREATE (moderator)-[addModeratorReport:FILED]->(report) - SET addModeratorReport.createdAt = toString(datetime()), addModeratorReport.reasonCategory = 'other', addModeratorReport.reasonDescription = 'Old DISABLED relations didn't enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.' -) -FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NOT NULL THEN [1] ELSE [] END | - CREATE (disabledResourceReporter)-[moveModeratorReport:FILED]->(report) - SET moveModeratorReport = existingFiledReport - DELETE existingFiledReport -) - -RETURN disabledResource {.id}; -" | cypher-shell - -echo " -// for FILED resources without DISABLED relation which are handled above, create new FILED-Report-BELONGS_TO structure -MATCH (reporter:User)-[oldReport:REPORTED]->(notDisabledResource) -WHERE notDisabledResource:User OR notDisabledResource:Comment OR notDisabledResource:Post -MERGE (report:Report)-[:BELONGS_TO]->(notDisabledResource) -ON CREATE SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false -CREATE (reporter)-[filed:FILED]->(report) -SET report = oldReport -DELETE oldReport - -RETURN notDisabledResource {.id}; -" | cypher-shell - diff --git a/neo4j/db_manipulation/add_image_aspect_ratio.sh b/neo4j/db_manipulation/add_image_aspect_ratio.sh deleted file mode 100755 index 8e2a16a01..000000000 --- a/neo4j/db_manipulation/add_image_aspect_ratio.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then - echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." - echo "Database manipulation is not possible without connecting to the database." - echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" -fi - -until echo 'RETURN "Connection successful" as info;' | cypher-shell -do - echo "Connecting to neo4j failed, trying again..." - sleep 1 -done - -echo " - CALL apoc.periodic.iterate(' - CALL apoc.load.csv("out.csv") yield map as row return row - ',' - MATCH (post:Post) where post.image = row.image - set post.imageAspectRatio = row.aspectRatio - ', {batchSize:10000, iterateList:true, parallel:true}); -" | cypher-shell diff --git a/neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh b/neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh deleted file mode 100755 index e611382f0..000000000 --- a/neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -ENV_FILE=$(dirname "$0")/.env -[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" - -if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then - echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." - echo "Database manipulation is not possible without connecting to the database." - echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" -fi - -until echo 'RETURN "Connection successful" as info;' | cypher-shell -do - echo "Connecting to neo4j failed, trying again..." - sleep 1 -done - -echo " - :begin - MATCH(user)-[reported:REPORTED]->(resource) - WITH reported, resource, COLLECT(user) as users - MERGE(report:Report)-[:BELONGS_TO]->(resource) - SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false - WITH report, users, reported - UNWIND users as user - MERGE (user)-[filed:FILED]->(report) - SET filed = reported - DELETE reported; - - MATCH(moderator)-[disabled:DISABLED]->(resource) - MATCH(report:Report)-[:BELONGS_TO]->(resource) - WITH disabled, resource, COLLECT(moderator) as moderators, report - DELETE disabled - WITH report, moderators, disabled - UNWIND moderators as moderator - MERGE (moderator)-[review:REVIEWED {disable: true}]->(report) - SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true; - - MATCH(moderator)-[disabled:DISABLED]->(resource) - WITH disabled, resource, COLLECT(moderator) as moderators - MERGE(report:Report)-[:BELONGS_TO]->(resource) - SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false - DELETE disabled - WITH report, moderators, disabled - UNWIND moderators as moderator - MERGE(moderator)-[filed:FILED]->(report) - SET filed.createdAt = toString(datetime()), filed.reasonCategory = 'other', filed.reasonDescription = 'Old DISABLED relations didn\'t enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.' - MERGE (moderator)-[review:REVIEWED {disable: true}]->(report) - SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true; - :commit -" | cypher-shell \ No newline at end of file From 73b005a900d43ccc639a3250ac77ae0ff4a54718 Mon Sep 17 00:00:00 2001 From: ogerly Date: Mon, 20 Jan 2020 11:18:35 +0100 Subject: [PATCH 014/107] test: added some test --- webapp/components/Comment/Comment.spec.js | 25 ++++++++++++++++--- webapp/components/Comment/Comment.vue | 11 ++++---- .../CommentList/CommentList.spec.js | 18 +++++++++++++ webapp/components/CommentList/CommentList.vue | 1 + webapp/locales/de.json | 2 +- webapp/locales/en.json | 2 +- webapp/pages/post/_id/_slug/index.spec.js | 19 ++++++++++++-- 7 files changed, 66 insertions(+), 12 deletions(-) diff --git a/webapp/components/Comment/Comment.spec.js b/webapp/components/Comment/Comment.spec.js index b307700d9..ea04d7c5b 100644 --- a/webapp/components/Comment/Comment.spec.js +++ b/webapp/components/Comment/Comment.spec.js @@ -1,4 +1,4 @@ -import { config, shallowMount } from '@vue/test-utils' +import { config, mount } from '@vue/test-utils' import Comment from './Comment.vue' import Vuex from 'vuex' @@ -47,14 +47,14 @@ describe('Comment.vue', () => { } }) - describe('shallowMount', () => { + describe('mount', () => { beforeEach(jest.useFakeTimers) Wrapper = () => { const store = new Vuex.Store({ getters, }) - return shallowMount(Comment, { + return mount(Comment, { store, propsData, mocks, @@ -68,6 +68,7 @@ describe('Comment.vue', () => { id: '2', contentExcerpt: 'Hello I am a comment content', content: 'Hello I am comment content', + author: { id: 'commentAuthorId', slug: 'ogerly'} } }) @@ -199,6 +200,24 @@ describe('Comment.vue', () => { }) }) }) + + describe('click reply button', () => { + + beforeEach(async () => { + wrapper = Wrapper() + await wrapper.find('.reply-button').trigger('click') + }) + it('emits "reply"', () => { + expect(wrapper.emitted('reply')).toEqual([ + [ + { + id: 'commentAuthorId', + slug: 'ogerly' + }, + ], + ]) + }) + }) }) }) }) diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue index b31414449..da205aaee 100644 --- a/webapp/components/Comment/Comment.vue +++ b/webapp/components/Comment/Comment.vue @@ -55,11 +55,11 @@ @@ -76,6 +76,7 @@ import HcCommentForm from '~/components/CommentForm/CommentForm' import CommentMutations from '~/graphql/CommentMutations' import scrollToAnchor from '~/mixins/scrollToAnchor.js' + export default { mixins: [scrollToAnchor], data() { @@ -148,7 +149,7 @@ export default { }, }, methods: { - reply(comment) { + reply() { const message = { slug: this.comment.author.slug, id: this.comment.author.id } this.$emit('reply', message) }, @@ -204,11 +205,11 @@ export default { float: right; } -.answerbutton { +.reply-button { float: right; top: 0px; } -.answerbutton:after { +.reply-button:after { clear: both; } diff --git a/webapp/components/CommentList/CommentList.spec.js b/webapp/components/CommentList/CommentList.spec.js index 0c037d2ff..0b4a667a7 100644 --- a/webapp/components/CommentList/CommentList.spec.js +++ b/webapp/components/CommentList/CommentList.spec.js @@ -103,4 +103,22 @@ describe('CommentList.vue', () => { }) }) }) + + describe('Comment', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('Comment emitted reply()', () => { + wrapper.find('.comment-tag').vm.$emit('reply') + expect(wrapper.emitted('reply')).toEqual([ + [ + { + id: 'commentAuthorId', + slug: 'ogerly' + }, + ], + ]) + }) + }) }) diff --git a/webapp/components/CommentList/CommentList.vue b/webapp/components/CommentList/CommentList.vue index d21f2b407..061210176 100644 --- a/webapp/components/CommentList/CommentList.vue +++ b/webapp/components/CommentList/CommentList.vue @@ -17,6 +17,7 @@ @deleteComment="updateCommentList" @updateComment="updateCommentList" @toggleNewCommentForm="toggleNewCommentForm" + class="comment-tag" /> diff --git a/webapp/locales/de.json b/webapp/locales/de.json index a394424cb..de9797596 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -270,7 +270,7 @@ "submit": "Kommentiere", "submitted": "Kommentar Gesendet", "updated": "Änderungen gespeichert", - "answer": "Antworten" + "reply": "Antworten" }, "edited": "bearbeitet" }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index e6e3426a7..9226793c1 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -424,7 +424,7 @@ "submit": "Comment", "submitted": "Comment Submitted", "updated": "Changes Saved", - "answer": "Answer" + "reply": "Reply" }, "edited": "edited" }, diff --git a/webapp/pages/post/_id/_slug/index.spec.js b/webapp/pages/post/_id/_slug/index.spec.js index db960bb67..d7c234c4c 100644 --- a/webapp/pages/post/_id/_slug/index.spec.js +++ b/webapp/pages/post/_id/_slug/index.spec.js @@ -1,16 +1,16 @@ import { config, shallowMount } from '@vue/test-utils' import PostSlug from './index.vue' import Vuex from 'vuex' +import CommentList from '~/components/CommentList/CommentList' const localVue = global.localVue -config.stubs['client-only'] = '' - describe('PostSlug', () => { let wrapper let Wrapper let store let mocks + let propsData beforeEach(() => { store = new Vuex.Store({ @@ -20,6 +20,7 @@ describe('PostSlug', () => { }, }, }) + propsData = {} mocks = { $t: jest.fn(), $filters: { @@ -44,12 +45,14 @@ describe('PostSlug', () => { } }) + describe('shallowMount', () => { Wrapper = () => { return shallowMount(PostSlug, { store, mocks, localVue, + propsData, }) } @@ -92,4 +95,16 @@ describe('PostSlug', () => { }) }) }) + + describe('given a comment', () => { + wrapper = Wrapper() + const bar = wrapper.find(CommentList) + it('hc-comment-list', () => { + expect(bar).toBe({"selector": "Component"}) + }) + + + + }) }) + From 98a4521ecc36cf44deebb2ba02fbbdf96d53b8c2 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 20 Jan 2020 11:28:36 +0100 Subject: [PATCH 015/107] Add back missing search constraint --- backend/src/db/migrate/store.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 9984b3971..b2d65a0f2 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -3,10 +3,27 @@ import { getDriver, getNeode } from '../../db/neo4j' class Store { async init(fn) { const neode = getNeode() - await getNeode().schema.install() + const { driver } = neode + const session = driver.session() // eslint-disable-next-line no-console - console.log('Successfully created database indices and constraints!') - neode.driver.close() + const writeTxResultPromise = session.writeTransaction(async txc => { + await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices + return Promise.all([ + 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', + 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])' + ].map(statement => txc.run(statement))) + }) + try { + await writeTxResultPromise + await getNeode().schema.install() + console.log('Successfully created database indices and constraints!') + } catch (error) { + console.log(error) // eslint-disable-line no-console + } finally { + session.close() + driver.close() + fn() + } } async load(fn) { From a86b26a756d47629c73adfb0e142f0d3b476dc35 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 20 Jan 2020 15:22:51 +0100 Subject: [PATCH 016/107] Various fixes for data migrations * Add unique index for `Migration`s * Fix proper use of `next` callback. First argument is potential error. * Update migration template --- backend/package.json | 8 +++--- backend/src/db/migrate/store.js | 28 +++++++++++-------- backend/src/db/migrate/template.js | 28 +++++++++++++++++-- ...387929122-merge_duplicate_user_accounts.js | 6 ++-- backend/src/models/Migration.js | 5 ++++ backend/src/models/index.js | 1 + 6 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 backend/src/models/Migration.js diff --git a/backend/package.json b/backend/package.json index 3c9fda287..b87091a1b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,8 +4,10 @@ "description": "GraphQL Backend for Human Connection", "main": "src/index.js", "scripts": { - "build": "babel src/ -d dist/ --copy-files", + "__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations", + "prod:migrate": "migrate --migrations-dir ./dist/db/migrations --store ./dist/db/migrate/store.js", "start": "node dist/", + "build": "babel src/ -d dist/ --copy-files", "dev": "nodemon --exec babel-node src/ -e js,gql", "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql", "lint": "eslint src --config .eslintrc.js", @@ -13,10 +15,8 @@ "db:clean": "babel-node src/db/clean.js", "db:reset": "yarn run db:clean", "db:seed": "babel-node src/db/seed.js", - "__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations", "db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js", - "db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js create", - "production:db:migrate": "migrate --migrations-dir ./dist/db/migrations --store ./dist/db/migrate/store.js" + "db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js create" }, "author": "Human Connection gGmbH", "license": "MIT", diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index b2d65a0f2..97b057dac 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -1,32 +1,36 @@ import { getDriver, getNeode } from '../../db/neo4j' class Store { - async init(fn) { + async init(next) { const neode = getNeode() const { driver } = neode const session = driver.session() // eslint-disable-next-line no-console const writeTxResultPromise = session.writeTransaction(async txc => { await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices - return Promise.all([ - 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', - 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])' - ].map(statement => txc.run(statement))) + return Promise.all( + [ + 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', + 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])', + ].map(statement => txc.run(statement)), + ) }) try { await writeTxResultPromise await getNeode().schema.install() + // eslint-disable-next-line no-console console.log('Successfully created database indices and constraints!') + next() } catch (error) { console.log(error) // eslint-disable-line no-console + next(error, null) } finally { session.close() driver.close() - fn() } } - async load(fn) { + async load(next) { const driver = getDriver() const session = driver.session() const readTxResultPromise = session.readTransaction(async txc => { @@ -42,18 +46,19 @@ class Store { console.log( "No migrations found in database. If it's the first time you run migrations, then this is normal.", ) - return fn(null, {}) + return next(null, {}) } const [{ title: lastRun }] = migrations - fn(null, { lastRun, migrations }) + next(null, { lastRun, migrations }) } catch (error) { console.log(error) // eslint-disable-line no-console + next(error) } finally { session.close() } } - async save(set, fn) { + async save(set, next) { const driver = getDriver() const session = driver.session() const { migrations } = set @@ -70,11 +75,12 @@ class Store { }) try { await writeTxResultPromise + next() } catch (error) { console.log(error) // eslint-disable-line no-console + next(error) } finally { session.close() - fn() } } } diff --git a/backend/src/db/migrate/template.js b/backend/src/db/migrate/template.js index 941f2a9e3..b8511e9bb 100644 --- a/backend/src/db/migrate/template.js +++ b/backend/src/db/migrate/template.js @@ -1,7 +1,31 @@ +import { getDriver } from '../../db/neo4j' + +export const description = '' + export function up(next) { - next() + const driver = getDriver() + const session = driver.session() + try { + // Implement your migration here. + next() + } catch (err) { + next(err) + } finally { + session.close() + driver.close() + } } export function down(next) { - next() + const driver = getDriver() + const session = driver.session() + try { + // Rollback your migration here. + next() + } catch (err) { + next(err) + } finally { + session.close() + driver.close() + } } diff --git a/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js b/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js index 9dbc8ad05..ec38befc5 100644 --- a/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js +++ b/backend/src/db/migrations/1579387929122-merge_duplicate_user_accounts.js @@ -74,11 +74,11 @@ export function up(next) { next() }, error: error => { - throw new Error(error) + next(new Error(error), null) }, }) } -export function down() { - throw new Error('Irreversible migration') +export function down(next) { + next(new Error('Irreversible migration')) } diff --git a/backend/src/models/Migration.js b/backend/src/models/Migration.js new file mode 100644 index 000000000..e36d10ac3 --- /dev/null +++ b/backend/src/models/Migration.js @@ -0,0 +1,5 @@ +export default { + title: { type: 'string', primary: true, token: true }, + description: { type: 'string' }, + timestamp: { type: 'number', unique: true }, +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 047ace67c..dbb6a927e 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -13,4 +13,5 @@ export default { Location: require('./Location.js').default, Donations: require('./Donations.js').default, Report: require('./Report.js').default, + Migration: require('./Migration.js').default, } From c7ee90e9800903402ea5db261112f7655b932ab0 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 21 Jan 2020 18:50:46 +0100 Subject: [PATCH 017/107] Follow @roschaefer PR suggestions - As blocking is now reciprocal, we do not need another query, we can use neo4j-graphql-js magic to query for a BLOCKED relationship between the postAuthor and the currentUser --- .../src/middleware/permissionsMiddleware.js | 1 - backend/src/schema/resolvers/users.js | 23 ------------------- backend/src/schema/types/type/User.gql | 3 +-- webapp/graphql/PostQuery.js | 1 + webapp/graphql/User.js | 7 ------ webapp/pages/post/_id/_slug/index.vue | 17 +------------- 6 files changed, 3 insertions(+), 49 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 12a557232..3b42ae7fe 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -102,7 +102,6 @@ export default shield( blockedUsers: isAuthenticated, notifications: isAuthenticated, Donations: isAuthenticated, - blockedByPostAuthor: isAuthenticated, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index c2600ab24..0b3f13631 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -48,29 +48,6 @@ export default { throw new UserInputError(e.message) } }, - blockedByPostAuthor: async (_parent, params, context, _resolveInfo) => { - const { postAuthorId } = params - const { user, driver } = context - const session = driver.session() - const readTxResultPromise = session.readTransaction(async transaction => { - const blockedByPostAuthorTransactionResponse = await transaction.run( - ` - MATCH (currentUser:User {id: $currentUserId})<-[relationship:BLOCKED]-(postAuthor:User {id: $postAuthorId}) - RETURN COUNT(relationship) >= 1 as blockedByPostAuthor - `, - { postAuthorId, currentUserId: user.id }, - ) - return blockedByPostAuthorTransactionResponse.records.map(record => - record.get('blockedByPostAuthor'), - ) - }) - try { - const [blockedByPostAuthor] = await readTxResultPromise - return blockedByPostAuthor - } finally { - session.close() - } - }, User: async (object, args, context, resolveInfo) => { const { email } = args if (email) { diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 17ee2a733..715a1f3e1 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -71,7 +71,7 @@ type User { isBlocked: Boolean! @cypher( statement: """ - MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId}) + MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId}) RETURN COUNT(user) >= 1 """ ) @@ -160,7 +160,6 @@ type Query { orderBy: [_UserOrdering] filter: _UserFilter ): [User] - blockedByPostAuthor(postAuthorId: ID!): Boolean! blockedUsers: [User] currentUser: User } diff --git a/webapp/graphql/PostQuery.js b/webapp/graphql/PostQuery.js index c59c894a5..a3ac8345a 100644 --- a/webapp/graphql/PostQuery.js +++ b/webapp/graphql/PostQuery.js @@ -29,6 +29,7 @@ export default i18n => { ...user ...userCounts ...locationAndBadges + isBlocked } comments(orderBy: createdAt_asc) { ...comment diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index fa4b47423..4ed832ad3 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -216,10 +216,3 @@ export const checkSlugAvailableQuery = gql` } } ` -export const blockedByPostAuthor = () => { - return gql` - query($postAuthorId: ID!) { - blockedByPostAuthor(postAuthorId: $postAuthorId) - } - ` -} diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index bdfa13cf2..41e9f4503 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -90,7 +90,7 @@ /> @@ -120,7 +120,6 @@ import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostH import PostQuery from '~/graphql/PostQuery' import HcEmotions from '~/components/Emotions/Emotions' import PostMutations from '~/graphql/PostMutations' -import { blockedByPostAuthor } from '~/graphql/User' export default { name: 'PostSlug', @@ -231,20 +230,6 @@ export default { }, fetchPolicy: 'cache-and-network', }, - blockedByPostAuthor: { - query() { - return blockedByPostAuthor() - }, - variables() { - return { - postAuthorId: this.postAuthor ? this.postAuthor.id : this.$store.getters['auth/user'].id, - } - }, - update({ blockedByPostAuthor }) { - this.blocked = blockedByPostAuthor - }, - fetchPolicy: 'cache-and-network', - }, }, } From bcae52180e3ff5ae7ecf9de31800aa691e47a648 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 21 Jan 2020 19:28:03 +0100 Subject: [PATCH 018/107] Remove filter on posts for BLOCKED --- backend/src/schema/resolvers/posts.js | 29 +-------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 619b1de25..72af86b4f 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -1,34 +1,10 @@ import uuid from 'uuid/v4' import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' -import { getBlockedUsers, getBlockedByUsers } from './users.js' -import { mergeWith, isArray, isEmpty } from 'lodash' +import { isEmpty } from 'lodash' import { UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' -const filterForBlockedUsers = async (params, context) => { - if (!context.user) return params - const [blockedUsers, blockedByUsers] = await Promise.all([ - getBlockedUsers(context), - getBlockedByUsers(context), - ]) - const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)] - if (!badIds.length) return params - - params.filter = mergeWith( - params.filter, - { - author_not: { id_in: badIds }, - }, - (objValue, srcValue) => { - if (isArray(objValue)) { - return objValue.concat(srcValue) - } - }, - ) - return params -} - const maintainPinnedPosts = params => { const pinnedPostFilter = { pinned: true } if (isEmpty(params.filter)) { @@ -42,16 +18,13 @@ const maintainPinnedPosts = params => { export default { Query: { Post: async (object, params, context, resolveInfo) => { - // params = await filterForBlockedUsers(params, context) params = await maintainPinnedPosts(params) return neo4jgraphql(object, params, context, resolveInfo) }, findPosts: async (object, params, context, resolveInfo) => { - params = await filterForBlockedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) }, profilePagePosts: async (object, params, context, resolveInfo) => { - params = await filterForBlockedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) }, PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { From 2df4e463f89b988eb5e37f55ab1eedd688423909 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 21 Jan 2020 19:28:19 +0100 Subject: [PATCH 019/107] Update/Add translations for placeholder message - @alina-beck, I had a look at the Placeholder component and it's quite simple, but I guess migrating that is a separate PR, but I don't know aobut adding more ds-placeholders, do you have some design ideas on what could look better? --- webapp/locales/de.json | 13 +++++++------ webapp/locales/en.json | 9 ++++----- webapp/pages/post/_id/_slug/index.vue | 14 ++++++-------- webapp/pages/settings/blocked-users.vue | 3 --- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 1417d3a51..03fcc0f74 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -149,11 +149,12 @@ "name": "Blockierte Benutzer", "explanation": { "intro": "Wenn ein anderer Benutzer von dir blockiert wurde, dann passiert folgendes:", - "your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der blockierten Person mehr auf.", - "their-perspective": "Umgekehrt das gleiche: Die blockierte Person sieht deine Beiträge auch nicht mehr in ihrer Übersicht.", - "search": "Die Beiträge von blockierten Personen verschwinden aus deinen Suchergebnissen.", - "notifications": "Von dir blockierte Personen erhalten keine Benachrichtigungen mehr, wenn sie in deinen Beiträgen erwähnt werden.", - "closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer dich nicht mehr länger belästigen können." + "your-perspective": "Sie werden nicht mehr in der Lage sein, mit ihren Beiträgen zu interagieren.", + "their-perspective": "Umgekehrt das gleiche: Die blockierte Person kann auch nicht mehr mit Ihren Beiträgen interagieren.", + "notifications": "Gesperrte Benutzer erhalten keine Benachrichtigungen mehr, wenn sie sich gegenseitig erwähnen.", + "closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer dich nicht mehr länger belästigen können.", + "commenting-disabled": "Ein Kommentar zu diesem Beitrag ist zur Zeit nicht möglich.", + "commenting-explanation": "Dies kann aus verschiedenen Gründen geschehen, siehe " }, "columns": { "name": "Name", @@ -485,7 +486,7 @@ }, "teaserImage": { "cropperConfirm": "Bestätigen" - }, + }, "inappropriatePicture" : "Dieses Bild kann für einige Menschen unangemessen sein.", "inappropriatePictureText" : "Wann soll ein Foto versteckt werden" }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 7d970998d..330497a3c 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -313,13 +313,12 @@ "name": "Blocked users", "explanation": { "intro": "If another user has been blocked by you, this is what happens:", - "your-perspective": "The blocked person's posts will no longer appear in your news feed.", - "their-perspective": "Vice versa: The blocked person will also no longer see your posts in their news feed.", - "search": "Posts of blocked people disappear from your search results.", - "notifications": "Blocked users will no longer receive notifications if they are mentioned in your posts.", + "your-perspective": "You will no longer be able to interact with their contributions.", + "their-perspective": "Vice versa: The blocked person will also no longer be able to interact with your contributions.", + "notifications": "Blocked users will no longer receive notifications if they mention each other.", "closing": "This should be sufficient for now so that blocked users can no longer bother you.", "commenting-disabled": "Commenting is not possible at this time on this post.", - "commenting-explanation": "This can happen for several reasons, please see " + "commenting-explanation": "This can happen for several reasons, please see our " }, "columns": { "name": "Name", diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index 41e9f4503..f0757e5a3 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -94,14 +94,12 @@ :post="post" @createComment="createComment" /> - - - {{ $t('settings.blocked-users.explanation.commenting-disabled') }} -
- {{ $t('settings.blocked-users.explanation.commenting-explanation') }} - https://human-connection.org -
-
+ + {{ $t('settings.blocked-users.explanation.commenting-disabled') }} +
+ {{ $t('settings.blocked-users.explanation.commenting-explanation') }} + FAQ +
diff --git a/webapp/pages/settings/blocked-users.vue b/webapp/pages/settings/blocked-users.vue index acbd253aa..db7dd9f93 100644 --- a/webapp/pages/settings/blocked-users.vue +++ b/webapp/pages/settings/blocked-users.vue @@ -12,9 +12,6 @@ {{ $t('settings.blocked-users.explanation.their-perspective') }} - - {{ $t('settings.blocked-users.explanation.search') }} - {{ $t('settings.blocked-users.explanation.notifications') }} From e639358557461dfd8c32be4a6d03cb0e879a5014 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 21 Jan 2020 21:16:42 +0100 Subject: [PATCH 020/107] Test drive to get the functionality how we'd like --- backend/src/schema/resolvers/searches.js | 2 +- backend/src/schema/resolvers/users.js | 22 +++++++++ cypress/integration/common/steps.js | 32 ++++++++++++- .../user_profile/BlockUser.feature | 48 +++++++++++++++++++ .../user_profile/mute-users/Mute.feature | 5 +- webapp/components/ContentMenu/ContentMenu.vue | 17 +++++++ webapp/locales/en.json | 4 +- webapp/pages/profile/_id/_slug.vue | 23 ++++++++- 8 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 cypress/integration/user_profile/BlockUser.feature diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 5316ccd9a..bfbdd21b1 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -20,7 +20,7 @@ export default { AND NOT ( author.deleted = true OR author.disabled = true OR resource.deleted = true OR resource.disabled = true - OR (:User { id: $thisUserId })-[:BLOCKED]-(author) + OR (:User { id: $thisUserId })-[:MUTED]-(author) ) WITH resource, author, [(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments, diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index dc962dc00..915d8b6f8 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -23,6 +23,21 @@ export const getMutedUsers = async context => { return mutedUsers } +export const getBlockedUsers = async context => { + const { neode } = context + const userModel = neode.model('User') + let blockedUsers = neode + .query() + .match('user', userModel) + .where('user.id', context.user.id) + .relationship(userModel.relationships().get('blocked')) + .to('blocked', userModel) + .return('blocked') + blockedUsers = await blockedUsers.execute() + blockedUsers = blockedUsers.records.map(r => r.get('blocked').properties) + return blockedUsers +} + export default { Query: { mutedUsers: async (object, args, context, resolveInfo) => { @@ -32,6 +47,13 @@ export default { throw new UserInputError(e.message) } }, + blockedUsers: async (object, args, context, resolveInfo) => { + try { + return getBlockedUsers(context) + } catch (e) { + throw new UserInputError(e.message) + } + }, User: async (object, args, context, resolveInfo) => { const { email } = args if (email) { diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 53b5f7c3d..29f2fa85a 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -39,7 +39,7 @@ Given("I am logged in", () => { cy.login(loginCredentials); }); -Given("I am logged in as the muted user", () => { +Given("I am logged in as the {string} user", _ => { cy.login({ email: annoyingParams.email, password: '1234' }); }); @@ -123,6 +123,12 @@ When("I visit the {string} page", page => { cy.openPage(page); }); +When("the blocked user visits my post", () => { + cy.logout() + .login({ email: annoyingParams.email, password: annoyingParams.password }) + .openPage('/post/previously-created-post') +}) + Given("I am on the {string} page", page => { cy.openPage(page); }); @@ -485,7 +491,7 @@ Given("I follow the user {string}", name => { }); }); -Given('"Spammy Spammer" wrote a post {string}', title => { +Given('{string} wrote a post {string}', (_, title) => { cy.createCategories("cat21") .factory() .create("Post", { @@ -532,6 +538,20 @@ When("I mute the user {string}", name => { }); }); +When("I block the user {string}", name => { + cy.neode() + .first("User", { + name + }) + .then(blockedUser => { + cy.neode() + .first("User", { + name: narratorParams.name + }) + .relateTo(blockedUser, "blocked"); + }); +}); + When("I log in with:", table => { const [firstRow] = table.hashes(); const { @@ -550,3 +570,11 @@ Then("I see only one post with the title {string}", title => { .should("have.length", 1); cy.get(".main-container").contains(".post-link", title); }); + +Then("they should not see the comment from", () => { + cy.get(".ds-card-footer").children().should('not.have.class', 'comment-form') +}) + +Then("they should see a text explaining commenting is not possible", () => { + cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.") +}) \ No newline at end of file diff --git a/cypress/integration/user_profile/BlockUser.feature b/cypress/integration/user_profile/BlockUser.feature new file mode 100644 index 000000000..a04455b53 --- /dev/null +++ b/cypress/integration/user_profile/BlockUser.feature @@ -0,0 +1,48 @@ +Feature: Block a User + As a user + I'd like to have a button to block another user + To prevent him from seeing and interacting with my contributions + + Background: + Given I have a user account + And there is an annoying user called "Harassing User" + And I am logged in + + Scenario: Block a user + Given I am on the profile page of the annoying user + When I click on "Block user" from the content menu in the user info box + And I navigate to my "Blocked users" settings page + Then I can see the following table: + | Avatar | Name | + | | Harassing User | + + Scenario: Blocked user cannot interact with my contributions + Given I block the user "Harassing User" + And I previously created a post + And the blocked user visits my post + Then they should not see the comment from + And they should see a text explaining commenting is not possible + + Scenario: Block a previously followed user + Given I follow the user "Harassing User" + When I visit the profile page of the annoying user + And I click on "Block user" from the content menu in the user info box + And nobody is following the user profile anymore + + Scenario: Posts of blocked users are not filtered from search results + Given "Harassing User" wrote a post "You can still see my posts" + And I block the user "Harassing User" + When I search for "see" + Then I should see the following posts in the select dropdown: + | title | + | You can still see my posts | + + Scenario: Blocked users can still see my posts + Given I previously created a post + And I block the user "Harassing User" + Given I log out + And I am logged in as the "blocked" user + When I search for "previously created" + Then I should see the following posts in the select dropdown: + | title | + | previously created post | diff --git a/cypress/integration/user_profile/mute-users/Mute.feature b/cypress/integration/user_profile/mute-users/Mute.feature index b52faeeaa..cd7d1c827 100644 --- a/cypress/integration/user_profile/mute-users/Mute.feature +++ b/cypress/integration/user_profile/mute-users/Mute.feature @@ -1,8 +1,7 @@ Feature: Mute a User As a user I'd like to have a button to mute another user - To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts - + To prevent him from seeing and interacting with my contributions Background: Given I have a user account And there is an annoying user called "Spammy Spammer" @@ -46,7 +45,7 @@ Feature: Mute a User Given I previously created a post And I mute the user "Spammy Spammer" Given I log out - And I am logged in as the muted user + And I am logged in as the "muted" user When I search for "previously created" Then I should see the following posts in the select dropdown: | title | diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index a22bc3267..31d90a614 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -172,6 +172,23 @@ export default { icon: 'user-times', }) } + if (this.resource.isBlocked) { + routes.push({ + label: this.$t(`settings.blocked-users.unblock`), + callback: () => { + this.$emit('unblock', this.resource) + }, + icon: 'user-plus', + }) + } else { + routes.push({ + label: this.$t(`settings.blocked-users.block`), + callback: () => { + this.$emit('block', this.resource) + }, + icon: 'user-times', + }) + } } } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 2191a91e7..e36214d8a 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -337,7 +337,9 @@ "your-perspective": "You will no longer be able to interact with their contributions.", "their-perspective": "Vice versa: The blocked person will also no longer be able to interact with your contributions.", "notifications": "Blocked users will no longer receive notifications if they mention each other.", - "closing": "This should be sufficient for now so that blocked users can no longer bother you." + "closing": "This should be sufficient for now so that blocked users can no longer bother you.", + "commenting-disabled": "Commenting is not possible at this time on this post.", + "commenting-explanation": "This can happen for several reasons, please see our " }, "columns": { "name": "Name", diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index ce220b66b..01190caa2 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -24,6 +24,8 @@ class="user-content-menu" @mute="muteUser" @unmute="unmuteUser" + @block="blockUser" + @unblock="unblockUser" /> @@ -67,7 +69,7 @@ @@ -127,6 +127,10 @@ export default { display: flex; align-items: center; padding-left: $space-xx-small; + + > .user-avatar { + margin-right: $space-xx-small; + } } .avatar-menu-popover { padding-top: $space-x-small; diff --git a/webapp/components/UserTeaser/UserTeaser.vue b/webapp/components/UserTeaser/UserTeaser.vue index 9d1b22fd9..2057a8fcd 100644 --- a/webapp/components/UserTeaser/UserTeaser.vue +++ b/webapp/components/UserTeaser/UserTeaser.vue @@ -1,6 +1,6 @@