-
-
Notifications
You must be signed in to change notification settings - Fork 821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for GraphQL subscriptions in mergeSchema #420
Comments
Yeah, that's currently not supported, but let's start talking about how to make it happen! |
Steps to make it happen:
|
Getting access to the parent schema's pub sub engine seems like the tricky part. I wonder what performance implications/overhead would come from having some kind of proxying passthrough. Option to connect directly to the pub sub? |
@tim-soft I'm more and more of the opinion that while we can provide all the schema level subscription parts, we will have to just make users write custom code to handle proxying / pass through. |
Hi guys, would it be at least possible to add support for subscriptions defined in local schema? When I use:
root type Subscription which is defined in localSchema is missing. :( Many thanks for you response |
I've been going over this issue and it doesn't seem in most cases that the local instance would need access to the remote pubsub engine. If the local instance actually needed access to the remote pubsub, users could handle that in custom code using either the redis or mqtt engines. What I believe could work is an implementation using the existing Now I'm not familiar with the internals of schema merging, but how I imagine the rest of the implementation would work is as follows.
Below is a very general example of how the API would work using lots of copy-paste from the docs and pretending top-level await is a thing. import express from 'express';
import {
graphqlExpress,
graphiqlExpress,
} from 'apollo-server-express';
import bodyParser from 'body-parser';
import cors from 'cors';
import { execute } from 'graphql';
import { createServer } from 'http';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
import { split } from 'apollo-link';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
// Create an http link:
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
});
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: `ws://localhost:5000/`,
options: {
reconnect: true
}
});
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
const schema = await introspectSchema(link);
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
});
const PORT = 3000;
const server = express();
server.use('*', cors({ origin: `http://localhost:${PORT}` }));
server.use('/graphql', bodyParser.json(), graphqlExpress({
schema
}));
server.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
subscriptionsEndpoint: `ws://localhost:${PORT}/subscriptions`
}));
// Wrap the Express server
const ws = createServer(server);
ws.listen(PORT, () => {
console.log(`Apollo Server is now running on http://localhost:${PORT}`);
// Set up the WebSocket for handling GraphQL subscriptions
new SubscriptionServer({
execute,
schema
}, {
server: ws,
path: '/subscriptions',
});
}); |
How does the intermediate service know what client to push the socket message to? Are you suggesting that we set up a new websocket connection from the intermediate service to the originating service for every client connection? That seems like it would put a fair amount of extra pressure on the intermediate service. |
why for every client? there should be enough a single socket connection to which operating service should push all the events, and intermediate should handle subscriptions and pushing to clients |
@stubailo just merged a subscriptions schema, with another local schema, and a remote schema and everything worked as intended. |
@mfix22 So, it should work? Can you explain how you did that? When I mock subscriptions with a local schema (via
However, when I wrap the local schema with
Codeimport { makeExecutableSchema, mergeSchemas } from 'graphql-tools'
import gql from 'graphql-tag';
import {ApolloClient} from 'apollo-client';
import {SchemaLink} from 'apollo-link-schema';
import {InMemoryCache} from 'apollo-cache-inmemory';
const typeDefs = `
schema {
query: Query
subscription: Subscription
}
type Query {
foo: String
}
type Subscription {
bar: String,
}
`;
const localSchema = makeExecutableSchema({
typeDefs,
resolvers: {
Subscription: {
bar: () => 'yo',
},
},
});
// this works ...
// const schema = localSchema;
// ... but this doesn't work
const schema = mergeSchemas({
schemas: [localSchema],
});
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new SchemaLink({ schema }),
});
const query = gql`
subscription {
bar
}
`;
client
.subscribe({ query })
.subscribe(console.log) |
Found a solution, based on suggestion by @jtmthf
Code const mergedSchemaLink = new SchemaLink({ schema: mergedSchema });
const subscriptionLink = new SchemaLink({ schema: subscriptionSchema });
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
subscriptionLink,
mergedSchemaLink,
);
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link,
}); I realise this is actually a bit off topic, but maybe this work around helps others dealing with same issue. |
How do you do If you need to subscribe from the server ? @godspeedelbow |
@mlescaudron can you explain your use case a bit more, I am slightly confused to what you are asking me |
I would like to have another server to subscribe to graphql. But I can't figure out how is it possible |
Well, that server would effectively be/have a GraphQL client connecting to the GraphQL server. There's no limitations really whether the client is in the browser or a node process |
Still doesn't cover the case of subscription forwarding. Client1 subscribes to server1 which delegates the subscription to server2 by means of schema merging. Server2 pushes an update, which server1 gets...but then server A needs to know which client to push that to. Client1 -> Server1 -> Server2 -> Server1 -> Client1 Seems like attaching some meta to the request with a client ID that gets returned on the push is the simplest solution. Should be easy as the graphql spec has extensions built in. For the more complicated case of Client -> Server1 -> Server2 -> ... -> ServerN-1 -> ServerN -> ServerN-1 -> ... Server2 -> Server1 -> Client It seems a stack of identifiers is probably the more flexible option, with the subscribing server pushing an identifier onto the subscription request stack and popping from the update push stack. |
@godspeedelbow AFAIK, schema-link doesn't support subscriptions: apollographql/apollo-link#374 So I'm not sure how this would work in the snippet you posted: const subscriptionLink = new SchemaLink({ schema: subscriptionSchema }); |
I have done investigation in to this, and you can indeed use mergeSchema with subscriptions. But there is a problem. Resolvers do not work properly, graphql-tools is doing something strange here: the underlying javascript looks like this: changing it to: allows you to properly receive the data you are expecting, transformed and everything. So, modifying graphql-tools to remove the object spreading seems to fix the issue. I'm not sure if that is a valid solution however. |
@ericlewis Are you sure it works with remote schemas? Asking this because whenever I serve a stitched child of two schemas that have subscriptions, I get this kind of an error on trying to make a subscription {
"errors": [
{
"message": "Expected Iterable, but did not find one for field Subscription.user.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"user"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
],
"data": null
} All I can make out of this error is that the type expected was an array, but it did not get an array from the parent API. But if I directly try to run a subscription over my parent API, it works totally fine. Here is my code: import {
makeRemoteExecutableSchema,
introspectSchema,
mergeSchemas,
} from 'graphql-tools';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { ApolloServer } from 'apollo-server';
import fetch from 'node-fetch';
import ws from 'ws';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
const graphqlEndpoint = 'https://bazookaand.herokuapp.com/v1alpha1/graphql';
const makeWsLink = function (uri) {
return new WebSocketLink(new SubscriptionClient(
uri,
{ reconnect: true },
ws
));
};
// create executable schemas from remote GraphQL APIs
const createRemoteExecutableSchemas = async () => {
const httpLink = new HttpLink({
uri: graphqlEndpoint,
fetch
});
const wsLink = makeWsLink(graphqlEndpoint);
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
const remoteSchema = await introspectSchema(httpLink);
const remoteExecutableSchema = makeRemoteExecutableSchema({
schema: remoteSchema,
link
});
return remoteExecutableSchema;
};
const createNewSchema = async () => {
const schema = await createRemoteExecutableSchemas();
return mergeSchemas({
schemas: [schema]
});
};
const runServer = async () => {
// Get newly merged schema
const schema = await createNewSchema();
// start server with the new schema
const server = new ApolloServer({
schema
});
server.listen().then(() => console.log('4000'));
};
try {
runServer();
} catch (err) {
console.error(err);
} |
@wawhal I think you need to make a resolver for it. We are using it ourselves internally, but only one of our APIs has subscriptions the other does not, it def does work with remote schemas though using my fix. I also wrote a resolver because mine was spitting out the same error. |
@ericlewis Not really. The problem was that my subscription was returning an array and the delegation resolvers were converting array to objects: For example, it was converting:
to:
Which is why I was getting the error This pull request should fix the problem. Current workaround is to write a resolver that converts the object back to an array before return. |
@wawhal Im following ur implementation. but i get { |
Any updates on this? Having similar problem. Have merged two schemas and one of them have subscription. The result in the gateway is:
I tried to console.log the output of the transformedResult (look @ericlewis post above) and first I get the correct object and then it logs out null. Here is the result I get from the console.log
So it seems the actual data is coming but it isn't able to resolve it I guess? edit: I managed to fix it for me. I use the
|
This is a working example of remote schema with subscription by webscoket and query and mutation by http. Flow Client request Note
const wsLink = new ApolloLink(operation => {
// This is your context!
const context = operation.getContext().graphqlContext
// Create a new websocket link per request
return new WebSocketLink({
uri: "<YOUR_URI>",
options: {
reconnect: true,
connectionParams: { // give custom params to your websocket backend (e.g. to handle auth)
headers: {
authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
foo: 'bar'
}
},
},
webSocketImpl: ws,
}).request(operation)
// Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
})
const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
return {
headers: {
authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
},
}
}).concat(new HttpLink({
uri,
fetch,
}))
const link = split(
operation => {
const definition = getMainDefinition(operation.query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // <-- Executed if above function returns true
httpLink, // <-- Executed if above function returns false
)
const schema = await introspectSchema(link)
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
})
const server = new ApolloServer({
schema: mergeSchemas({ schemas: [ executableSchema, /* ...anotherschemas */] }),
context: ({ req, connection }) => {
let authorization;
if (req) { // when query or mutation is requested by http
authorization = req.headers.authorization
} else if (connection) { // when subscription is requested by websocket
authorization = connection.context.authorization
}
const token = authorization.replace('Bearer ', '')
return {
user: getUserFromToken(token),
}
},
}) |
Need Documentation of the use case of Merging Subscriptions from multiple GraphQL End Points.
If this is not available now, then we can discuss few scenarios in this thread and then open a specific Issue for that.
The text was updated successfully, but these errors were encountered: