Draft: Setup subsriptions for backend+frontend

Please clean up this commit and squash it later on.
This commit is contained in:
roschaefer 2020-01-24 17:25:45 +01:00
parent 1a93fcc023
commit 3be5dee08b
9 changed files with 102 additions and 16 deletions

View File

@ -96,6 +96,7 @@
"request": "~2.88.0", "request": "~2.88.0",
"sanitize-html": "~1.21.1", "sanitize-html": "~1.21.1",
"slug": "~2.1.0", "slug": "~2.1.0",
"subscriptions-transport-ws": "^0.9.16",
"trunc-html": "~1.1.2", "trunc-html": "~1.1.2",
"uuid": "~3.4.0", "uuid": "~3.4.0",
"validator": "^12.1.0", "validator": "^12.1.0",

View File

@ -49,6 +49,7 @@ export const serverConfigs = {
CLIENT_URI, CLIENT_URI,
GRAPHQL_URI, GRAPHQL_URI,
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true', PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true',
SUBSCRIPTIONS_PATH: '/subscriptions',
} }
export const developmentConfigs = { export const developmentConfigs = {

View File

@ -1,9 +1,10 @@
import createServer from './server' import createServer from './server'
import CONFIG from './config' import CONFIG from './config'
const { app } = createServer() const { app, server, httpServer } = createServer()
const url = new URL(CONFIG.GRAPHQL_URI) const url = new URL(CONFIG.GRAPHQL_URI)
app.listen({ port: url.port }, () => { httpServer.listen({ port: url.port }, () => {
/* eslint-disable-next-line no-console */ /* 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}`)
}) })

View File

@ -5,6 +5,11 @@ import { UserInputError } from 'apollo-server'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import { PubSub } from 'apollo-server'
const pubsub = new PubSub();
const POST_ADDED = 'POST_ADDED';
const maintainPinnedPosts = params => { const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true } const pinnedPostFilter = { pinned: true }
@ -17,6 +22,12 @@ const maintainPinnedPosts = params => {
} }
export default { export default {
Subscription: {
postAdded: {
// Additional event labels can be passed to asyncIterator creation
subscribe: () => pubsub.asyncIterator([POST_ADDED]),
},
},
Query: { Query: {
Post: async (object, params, context, resolveInfo) => { Post: async (object, params, context, resolveInfo) => {
params = await filterForMutedUsers(params, context) params = await filterForMutedUsers(params, context)
@ -102,6 +113,7 @@ export default {
}) })
try { try {
const [post] = await writeTxResultPromise const [post] = await writeTxResultPromise
pubsub.publish(POST_ADDED, { postAdded: post });
return post return post
} catch (e) { } catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')

View File

@ -1,3 +1,7 @@
type Subscription {
postAdded: Post
}
type Mutation { type Mutation {
# Get a JWT Token for the given Email and password # Get a JWT Token for the given Email and password
login(email: String!, password: String!): String! login(email: String!, password: String!): String!

View File

@ -1,6 +1,9 @@
import express from 'express' import express from 'express'
import http from 'http'
import helmet from 'helmet' import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import CONFIG from './config' import CONFIG from './config'
import middleware from './middleware' import middleware from './middleware'
import { getNeode, getDriver } from './db/neo4j' import { getNeode, getDriver } from './db/neo4j'
@ -8,19 +11,28 @@ import decode from './jwt/decode'
import schema from './schema' import schema from './schema'
import webfinger from './activitypub/routes/webfinger' import webfinger from './activitypub/routes/webfinger'
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
export const context = async ({ req }) => { const getContext = async (req) => {
const user = await decode(driver, req.headers.authorization) const user = await decode(driver, req.headers.authorization)
return { return {
driver, driver,
neode, neode,
user, user,
req, req,
cypherParams: { cypherParams: {
currentUserId: user ? user.id : null, 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 = { const defaults = {
context, context,
schema: middleware(schema), schema: middleware(schema),
subscriptions: {
onConnect: (connectionParams, webSocket) => {
console.log('connectionParams', connectionParams)
return getContext(connectionParams)
},
},
debug: !!CONFIG.DEBUG, debug: !!CONFIG.DEBUG,
tracing: !!CONFIG.DEBUG, tracing: !!CONFIG.DEBUG,
formatError: error => { formatError: error => {
@ -46,8 +64,11 @@ const createServer = options => {
app.use('/.well-known/', webfinger()) app.use('/.well-known/', webfinger())
app.use(express.static('public')) app.use(express.static('public'))
server.applyMiddleware({ app, path: '/' }) server.applyMiddleware({ app, path: '/' })
const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);
return { server, app }
return { server, httpServer, app }
} }
export default createServer export default createServer

View File

@ -1175,7 +1175,7 @@
url-regex "~4.1.1" url-regex "~4.1.1"
video-extensions "~1.1.0" video-extensions "~1.1.0"
"@metascraper/helpers@^5.10.5", "@metascraper/helpers@^5.10.6": "@metascraper/helpers@^5.10.6":
version "5.10.6" version "5.10.6"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.10.6.tgz#0b786607212925a577926fd0cd0313a49de3499c" resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.10.6.tgz#0b786607212925a577926fd0cd0313a49de3499c"
integrity sha512-/jvhlM3RKGYMoUK8D8S1r3tN03/EYizCqWF7zDx0aBMC8Ihp33DRGs9oNdsgkgwzVF7O/YpDm55l9K+qVJlsyQ== integrity sha512-/jvhlM3RKGYMoUK8D8S1r3tN03/EYizCqWF7zDx0aBMC8Ihp33DRGs9oNdsgkgwzVF7O/YpDm55l9K+qVJlsyQ==
@ -7918,6 +7918,11 @@ serve-static@1.14.1:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== 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: set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0" version "2.0.0"

View File

@ -74,6 +74,16 @@ import { mapGetters, mapMutations } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js' import { filterPosts } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations' import PostMutations from '~/graphql/PostMutations'
import UpdateQuery from '~/components/utils/UpdateQuery' import UpdateQuery from '~/components/utils/UpdateQuery'
import gql from 'graphql-tag'
import {
userFragment,
postFragment,
commentFragment,
postCountsFragment,
userCountsFragment,
locationAndBadgesFragment,
tagsCategoriesAndPinnedFragment,
} from '~/graphql/Fragments'
export default { export default {
components: { components: {
@ -213,6 +223,36 @@ export default {
this.posts = Post this.posts = Post
}, },
fetchPolicy: 'cache-and-network', 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
},
}
}, },
}, },
} }

View File

@ -9,6 +9,7 @@ export default ({ app }) => {
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000' const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
return { return {
wsEndpoint: 'ws://localhost:4000/graphql', // optional
httpEndpoint: process.server ? backendUrl : '/api', httpEndpoint: process.server ? backendUrl : '/api',
httpLinkOptions: { httpLinkOptions: {
credentials: 'same-origin', credentials: 'same-origin',
@ -16,7 +17,7 @@ export default ({ app }) => {
credentials: true, credentials: true,
tokenName: 'human-connection-token', tokenName: 'human-connection-token',
persisting: false, persisting: false,
websocketsOnly: false, websocketsOnly: true,
cache: new InMemoryCache({ fragmentMatcher }), cache: new InMemoryCache({ fragmentMatcher }),
} }
} }