From 3be5dee08b3a1853d86b7c01f768bf4920784be9 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Fri, 24 Jan 2020 17:25:45 +0100 Subject: [PATCH 01/21] Draft: Setup subsriptions for backend+frontend Please clean up this commit and squash it later on. --- backend/package.json | 1 + backend/src/config/index.js | 1 + backend/src/index.js | 7 +++-- backend/src/schema/resolvers/posts.js | 12 ++++++++ backend/src/schema/types/schema.gql | 4 +++ backend/src/server.js | 43 ++++++++++++++++++++------- backend/yarn.lock | 7 ++++- webapp/pages/index.vue | 40 +++++++++++++++++++++++++ webapp/plugins/apollo-config.js | 3 +- 9 files changed, 102 insertions(+), 16 deletions(-) diff --git a/backend/package.json b/backend/package.json index 2a83c7f01..d33f9e14a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -96,6 +96,7 @@ "request": "~2.88.0", "sanitize-html": "~1.21.1", "slug": "~2.1.0", + "subscriptions-transport-ws": "^0.9.16", "trunc-html": "~1.1.2", "uuid": "~3.4.0", "validator": "^12.1.0", diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 2f8d0ed22..59ab20af7 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -49,6 +49,7 @@ export const serverConfigs = { CLIENT_URI, GRAPHQL_URI, PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true', + SUBSCRIPTIONS_PATH: '/subscriptions', } export const developmentConfigs = { diff --git a/backend/src/index.js b/backend/src/index.js index 98354dc1f..0eb4f51b6 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,9 +1,10 @@ import createServer from './server' import CONFIG from './config' -const { app } = createServer() +const { app, server, httpServer } = createServer() const url = new URL(CONFIG.GRAPHQL_URI) -app.listen({ port: url.port }, () => { +httpServer.listen({ port: url.port }, () => { /* eslint-disable-next-line no-console */ - console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`) + console.log(`🚀 Server ready at http://localhost:${url.port}${server.graphqlPath}`) + console.log(`🚀 Subscriptions ready at ws://localhost:${url.port}${server.subscriptionsPath}`) }) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index af8165997..f8609f7a6 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -5,6 +5,11 @@ import { UserInputError } from 'apollo-server' import fileUpload from './fileUpload' import Resolver from './helpers/Resolver' import { filterForMutedUsers } from './helpers/filterForMutedUsers' +import { PubSub } from 'apollo-server' + +const pubsub = new PubSub(); +const POST_ADDED = 'POST_ADDED'; + const maintainPinnedPosts = params => { const pinnedPostFilter = { pinned: true } @@ -17,6 +22,12 @@ const maintainPinnedPosts = params => { } export default { + Subscription: { + postAdded: { + // Additional event labels can be passed to asyncIterator creation + subscribe: () => pubsub.asyncIterator([POST_ADDED]), + }, + }, Query: { Post: async (object, params, context, resolveInfo) => { params = await filterForMutedUsers(params, context) @@ -102,6 +113,7 @@ export default { }) try { const [post] = await writeTxResultPromise + pubsub.publish(POST_ADDED, { postAdded: post }); return post } catch (e) { if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 23c2ded4d..b3df61af2 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -1,3 +1,7 @@ +type Subscription { + postAdded: Post +} + type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! diff --git a/backend/src/server.js b/backend/src/server.js index 02e166b71..22431bd7f 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,6 +1,9 @@ import express from 'express' +import http from 'http' import helmet from 'helmet' import { ApolloServer } from 'apollo-server-express' + + import CONFIG from './config' import middleware from './middleware' import { getNeode, getDriver } from './db/neo4j' @@ -8,19 +11,28 @@ import decode from './jwt/decode' import schema from './schema' import webfinger from './activitypub/routes/webfinger' + const driver = getDriver() const neode = getNeode() -export const context = async ({ req }) => { - const user = await decode(driver, req.headers.authorization) - return { - driver, - neode, - user, - req, - cypherParams: { - currentUserId: user ? user.id : null, - }, +const getContext = async (req) => { + const user = await decode(driver, req.headers.authorization) + return { + driver, + neode, + user, + req, + cypherParams: { + currentUserId: user ? user.id : null, + }, + } +} +export const context = async (options) => { + const { connection, req } = options + if (connection) { + return connection.context + } else { + return getContext(req) } } @@ -28,6 +40,12 @@ const createServer = options => { const defaults = { context, schema: middleware(schema), + subscriptions: { + onConnect: (connectionParams, webSocket) => { + console.log('connectionParams', connectionParams) + return getContext(connectionParams) + }, + }, debug: !!CONFIG.DEBUG, tracing: !!CONFIG.DEBUG, formatError: error => { @@ -46,8 +64,11 @@ const createServer = options => { app.use('/.well-known/', webfinger()) app.use(express.static('public')) server.applyMiddleware({ app, path: '/' }) + const httpServer = http.createServer(app); + server.installSubscriptionHandlers(httpServer); - return { server, app } + + return { server, httpServer, app } } export default createServer diff --git a/backend/yarn.lock b/backend/yarn.lock index caf302820..9d3e0e571 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1175,7 +1175,7 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@metascraper/helpers@^5.10.5", "@metascraper/helpers@^5.10.6": +"@metascraper/helpers@^5.10.6": version "5.10.6" resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.10.6.tgz#0b786607212925a577926fd0cd0313a49de3499c" integrity sha512-/jvhlM3RKGYMoUK8D8S1r3tN03/EYizCqWF7zDx0aBMC8Ihp33DRGs9oNdsgkgwzVF7O/YpDm55l9K+qVJlsyQ== @@ -7918,6 +7918,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" diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index 609c3d800..f0089979d 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -74,6 +74,16 @@ import { mapGetters, mapMutations } from 'vuex' import { filterPosts } from '~/graphql/PostQuery.js' import PostMutations from '~/graphql/PostMutations' import UpdateQuery from '~/components/utils/UpdateQuery' +import gql from 'graphql-tag' +import { + userFragment, + postFragment, + commentFragment, + postCountsFragment, + userCountsFragment, + locationAndBadgesFragment, + tagsCategoriesAndPinnedFragment, +} from '~/graphql/Fragments' export default { components: { @@ -213,6 +223,36 @@ export default { this.posts = Post }, fetchPolicy: 'cache-and-network', + subscribeToMore: { + document: gql` + ${userFragment} + ${userCountsFragment} + ${locationAndBadgesFragment('EN')} + ${postFragment} + ${postCountsFragment} + ${tagsCategoriesAndPinnedFragment} + + subscription Post { + postAdded { + ...post + ...postCounts + ...tagsCategoriesAndPinned + author { + ...user + ...userCounts + ...locationAndBadges + } + } + }`, + // Mutate the previous result + updateQuery: (previousResult, { subscriptionData }) => { + console.log('previousResult', previousResult) + console.log('subscriptionData', subscriptionData) + const { data: { postAdded: newPost } } = subscriptionData + return { Post: [newPost, ...previousResult.Post] } + // Here, return the new result from the previous with the new data + }, + } }, }, } diff --git a/webapp/plugins/apollo-config.js b/webapp/plugins/apollo-config.js index 4bf05f178..a4487c0bb 100644 --- a/webapp/plugins/apollo-config.js +++ b/webapp/plugins/apollo-config.js @@ -9,6 +9,7 @@ export default ({ app }) => { const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000' return { + wsEndpoint: 'ws://localhost:4000/graphql', // optional httpEndpoint: process.server ? backendUrl : '/api', httpLinkOptions: { credentials: 'same-origin', @@ -16,7 +17,7 @@ export default ({ app }) => { credentials: true, tokenName: 'human-connection-token', persisting: false, - websocketsOnly: false, + websocketsOnly: true, cache: new InMemoryCache({ fragmentMatcher }), } } From a28eb8c91f60a13784f7ac666db4940747953056 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Fri, 24 Jan 2020 18:23:59 +0100 Subject: [PATCH 02/21] Start subscriptions for notifications --- .../notifications/notificationsMiddleware.js | 16 ++++++-- backend/src/schema/resolvers/notifications.js | 9 +++++ backend/src/schema/resolvers/posts.js | 6 +-- backend/src/schema/types/schema.gql | 4 -- .../src/schema/types/type/Subscription.gql | 4 ++ backend/src/server.js | 1 - .../NotificationMenu/NotificationMenu.vue | 21 +++++++---- webapp/graphql/User.js | 37 +++++++++++++++++++ webapp/pages/index.vue | 4 -- 9 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 backend/src/schema/types/type/Subscription.gql diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index 837193773..ca59be780 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -1,5 +1,9 @@ import extractMentionedUsers from './mentions/extractMentionedUsers' import { validateNotifyUsers } from '../validation/validationMiddleware' +import { PubSub } from 'apollo-server' + +const pubsub = new PubSub() +const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED' const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { const idsOfUsers = extractMentionedUsers(args.content) @@ -74,12 +78,18 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { WHEN notification.createdAt IS NULL THEN notification END ).createdAt = toString(datetime()) SET notification.updatedAt = toString(datetime()) + RETURN notification ` const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async transaction => { + const notificationTransactionResponse = await transaction.run(mentionedCypher, { id, idsOfUsers, reason }) + return notificationTransactionResponse.records.map(record => record.get('notification').properties) + }) try { - await session.writeTransaction(transaction => { - return transaction.run(mentionedCypher, { id, idsOfUsers, reason }) - }) + const [notification] = await writeTxResultPromise + return pubsub.publish(NOTIFICATION_ADDED, { notificationAdded: notification }) + } catch (error) { + throw new Error(error) } finally { session.close() } diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 31369a8c7..f92292bfc 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -1,4 +1,8 @@ import log from './helpers/databaseLogger' +import { PubSub } from 'apollo-server' + +const pubsub = new PubSub() +const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED' const resourceTypes = ['Post', 'Comment'] @@ -16,6 +20,11 @@ const transformReturnType = record => { } export default { + Subscription: { + notificationAdded: { + subscribe: () => pubsub.asyncIterator([NOTIFICATION_ADDED]), + }, + }, Query: { notifications: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index f8609f7a6..11f6eebc3 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -7,8 +7,8 @@ import Resolver from './helpers/Resolver' import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { PubSub } from 'apollo-server' -const pubsub = new PubSub(); -const POST_ADDED = 'POST_ADDED'; +const pubsub = new PubSub() +const POST_ADDED = 'POST_ADDED' const maintainPinnedPosts = params => { @@ -113,7 +113,7 @@ export default { }) try { const [post] = await writeTxResultPromise - pubsub.publish(POST_ADDED, { postAdded: post }); + pubsub.publish(POST_ADDED, { postAdded: post }) return post } catch (e) { if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index b3df61af2..23c2ded4d 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -1,7 +1,3 @@ -type Subscription { - postAdded: Post -} - type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! diff --git a/backend/src/schema/types/type/Subscription.gql b/backend/src/schema/types/type/Subscription.gql new file mode 100644 index 000000000..a2f8b11b9 --- /dev/null +++ b/backend/src/schema/types/type/Subscription.gql @@ -0,0 +1,4 @@ +type Subscription { + postAdded: Post + notificationAdded: NOTIFIED +} \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js index 22431bd7f..d20fa5e63 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -42,7 +42,6 @@ const createServer = options => { schema: middleware(schema), subscriptions: { onConnect: (connectionParams, webSocket) => { - console.log('connectionParams', connectionParams) return getContext(connectionParams) }, }, diff --git a/webapp/components/NotificationMenu/NotificationMenu.vue b/webapp/components/NotificationMenu/NotificationMenu.vue index a3b085db9..b18203b45 100644 --- a/webapp/components/NotificationMenu/NotificationMenu.vue +++ b/webapp/components/NotificationMenu/NotificationMenu.vue @@ -22,13 +22,13 @@