-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Allow for multiple remote endpoints #84
Comments
Personally, I think the main strength of GraphQL is having exactly one endpoint. If you're querying multiple GraphQL servers from the same client, I feel like you're doing it wrong (just my opinion at the moment). I think having a function in the server to proxy part of the schema to a different GraphQL server would be the right approach, so that you maintain the property that you can render your whole UI with one roundtrip. As for merging client and server data, I have a few ideas for how to do that, but I think that's a special case - it should be exactly one server schema and one client schema IMO. One main argument for me is that the whole point of GraphQL is to be able to traverse data in the same query, and have schema types that reference each other - just sending two root queries to different servers doesn't really give you any benefit over just initializing two clients IMO. We have some ideas for how this might work in a future Apollo proxy server, which would let you traverse between schemas at will in a single query, from your schema, to external services, etc. |
My understanding is that GraphQL is supposed to remove the need for versioning by having deprecated fields. So you can deprecate a field, then track how many clients access that field, and once it drops below a certain level you just remove it. Rather than having different versions of the whole API. |
@stubailo I haven't played around with deprecated fields. I'll give that a go for our use case. I can see how having the server doing the linking would be a better world too.
Thanks for the input 🎉 |
Yeah I would really love to know how that works for you. And it would be cool to collaborate on the proxy thing, I think that would be really valuable, if you could just have your schema say "oh, this is a GitHub user" and then be able to import the whole GitHub API schema and just traverse it. |
Moving this: #86 |
@stubailo since our move would be to the apollo version of the graphql server, I'm sure you'll hear about it haha.
The dream. Application libraries that you can import and use for your app |
We are in the process of rewriting the front end to existing old-school webservices, and it's quite naive to assume that a server will only ever have one endpoint. In our case we have two SOAP services (yep, old stuff) that provide data, and that's just "how it is" currently. In any case, as Apollo as of this writing doesn't seem to support our scenario as I can only tell apollo to use one endpoint based on my url, here's a quick and dirty copy & paste of what I did to make apollo + express handle this (Using Ang2 RC5 if it matters) in case someone else is interested. First, on the client side, I set up a middleware so that it takes a passed "variable" and sets it as part of the header, as follows: const networkInterface = createNetworkInterface('http://localhost:4000/graphql');
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {};
}
req.options.headers.service = req.request.variables["service"];
next();
}
}]); In my data provider service on the client, I then set up a basic fetch method like: private fetchData(service:string, query:any):any {
return new Promise((resolve, reject) => {
this.angularApollo.query({
query: query,
variables: {
service: service
}
}).then((graphQLResult) => this.handleGraphQLResponse(graphQLResult, (data) => resolve(data), (errors) => reject(errors)));
});
} This will ensure my passed in "service" string ends up on the request header sent to the server. Now, on the server, using express, I set up a redirect route that reads the header and sends the request on to either one or the other endpoint based on this header: graphQLServer.use('/graphql', function (req, res) {
// fetch service from header
var service = req.headers["service"];
var path = '';
if (service == 'trading') {
path = '/graphqlTrading'
}
else if (service == 'ref') {
path = '/graphqlRef'
}
else {
res.send('Unknown service in header, or not set. Service was: ' + service);
return;
}
var url = 'http://localhost:4000'+path;
// console.log("Piping '" + service + "' service " + req.method + " + request to " +url);
req.pipe(request(url)).pipe(res);
}); Do note however that bodyParser() can mess up redirects as it modifies the body on the request object and can cause the pipe to hang, in my case the express() I use for the GraphQL server is separate from the one I use for the other Ang2 server-side stuff and I don't include bodyParser unless specifically via: graphQLServer.use('/graphiqlRef', bodyParser.json(), graphiqlExpress({
endpointURL: '/graphqlRef'
})); In my opinion, it would be great if you could load up multiple network interfaces in apollo and at query-time be able to tell it which one I want it to use. I'm sure there's many possible solutions though. |
Were you able to work with ApolloStack in context and access your SOAP services? I have a similar case with using legacy SOAP services that we need to access with our Reactjs/Redux app ..we would like to go about this in the new "ApolloStack way" so further describing your success/experience with experimenting with SOAP using ApolloStack would be greatly appreciated and helpful to all of us! |
I'm pretty confident that the correct way to use GraphQL is to have one server that calls different services in the background. Having multiple ApolloClient instances is the best way to call multiple services. |
@yodacom, yes, we are successful using GraphQL and NodeJS together with npm module "soap". I wrote a quick and dirty java project to generate the GraphQL schema from a WSDL (requires some manual interaction post-generation). Since this issue is regarding something else, feel free to contact me if you are intesteted in it. I could put it up on github if others also are interested. |
@crumhorn I think it would be great if you could put it on GitHub, then we can link to it from here. |
@helfer, and anyone else interested, here it is: https://github.com/crumhorn/wsdl2graphql I wrote up a quick Readme.md for it. Like I mentioned earlier, it's a very quick and dirty project and I didn't spend much time on it, but it did the job for us, saving us a large amount of time. Do note the "Important" section, as there's two lines in the code that might necessary to change to fit other peoples web services. I suggest running it from either IntelliJ IDEA (or Eclipse) just to make it easy as getting the right classpath on the command line is (as always) a pain. |
Thanks Emil. Yeah the WSDL's are a great conversion project to get to All the best, Jeremy Yoda of YodaCom Standard Disclosure: On Wed, Sep 7, 2016 at 2:17 AM, Emil [email protected] wrote:
|
So, I was searching for a "createLocalInterface" (would do the same as https://github.com/relay-tools/relay-local-schema) for apollo-client and I ended up in this issue. For my app implementation I really need this. As I couldn't find it, I will have to do it. Is there any interest in having this on this project? Should I make a PR or do it in a separate library? |
Please do it in a separate library, you should be able to reuse basically all of the code from that Relay package, I think. What's the use case for you? Unable to run a GraphQL server so you need to do it on the client? |
It is app that uses a schema to normalise the data, but actually has drivers to multiple ecommece platforms (which are all rest). So independent of which backend, the app doesn't change, just the bundled driver. I've found another issue #379, concerning the same thing, it seems like a very common use case and is such a small piece of code. It also helps while prototyping. Please consider doing it in the client itself. |
What piece of code are you looking for in particular? Is it just the following?
Or is it more than just calling a schema on the client? |
I'm trying to find out if this is just something we can document, or if there is a lot of logic involved. |
Yep, it is just calling the schema on the client, that is all. Maybe documenting is enough though. |
@stubailo I just thought of this and stumbled upon this issue. I saw GitHub released a GraphQL API, and thought "how would I write a front-end app that connected to my own GraphQL backend, but also used GitHub's API?" I don't know of any way for Apollo to connect to multiple servers. |
@stubailo any chance you could provide an example of this without going to too much trouble?
|
@stubailo I have a similar use case to @ffxsam. I'm working on creating a prepackaged library that I'd like to distribute for other developers to use (so in my case imagine that I am Github and I'm trying to develop a client library that abstracts away the fact that the underlying implementation of my backend is graphql/apollo). Is there any way that I can create an HOC that utilizes ApolloProvider or similar without conflicting with code that's outside of the scope of my library? This would allow me to distribute an HOC in a library for other developers to use without accidentally smashing their code. Is there a way that I can change the prop that Apollo provider passes down so I could do something like
|
Maybe I was thinking about it the wrong way, and third party GraphQL APIs should be accessed on the backend, and ingested via resolvers? |
BTW this is our long-term solution to this: ardatan/graphql-tools#382 |
Add "Auth tokens" section. and correct syntax highlighting to "CORS" section as it appears to be missing.
there is also an interesting approach described here: https://medium.com/open-graphql/apollo-multiple-clients-with-react-b34b571210a5 he passes a different client to the query as a prop.. |
Why can't I specify multiple GraphQL endpoints? Because the main strength of GraphQL is having exactly one endpoint? What if I don't have a single one? I just want to write a frontend in which I will work with several public GraphQL endpoints. I don’t have my own server and I don’t have the slightest desire to raise the server and bother with the merging of the endpoints I need. And to solve this seemingly trivial task, there is nothing but crutches ... |
Assuming we can stitch data on the server is just plain wrong. You could say the same about REST APIs. Yes, in perfect world every app has one GraphQL endpoint perfectly designed for its needs. But this almost never happens. Developers in the real world need to deal with legacy data and code. |
Workaround for using of multiple GraphQL endpoints.We can use custom directives and write a Link that will select one of the Links to the GraphQL endpoints: import { ApolloLink } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
const allowedDirectives = ['dai', 'compound'] as const;
type DirectiveName = (typeof allowedDirectives)[number];
const linkByDirective: Record<DirectiveName | 'default', ApolloLink> = {
dai: daiLink,
compound: compoundLink,
default: daiLink,
};
const link = new ApolloLink(operation => {
const { query } = operation;
const definition = getMainDefinition(query);
const foundDirective =
'operation' in definition &&
definition.directives?.find(item =>
allowedDirectives.includes(item.name.value as DirectiveName),
);
const directive: DirectiveName | 'default' = foundDirective
? (foundDirective.name.value as DirectiveName)
: 'default';
return linkByDirective[directive].request(operation);
}); GraphQL query can be written like this query Users($first: Int!) @compound {
users(first: $first) {
id
cTokens {
id
cTokenBalance
}
totalSupplyInEth
totalBorrowInEth
}
} You can also use the graphql-codegen, but then you will need to write queries in separate files and the files will need to be named according to the template. For example, for the overwrite: true
generates:
src/generated/compound-graphql.tsx:
documents: "./src/**/*.compound.graphql"
schema: "https://api.thegraph.com/subgraphs/name/compound-finance/compound-v2-rinkeby"
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
config:
withComponent: false
withHOC: false
withHooks: true
reactApolloVersion: 3
scalars:
BigInt: string
BigDecimal: string
Bytes: string
src/generated/dai-graphql.tsx:
documents: "./src/**/*.dai.graphql"
schema: "https://api.thegraph.com/subgraphs/name/raisehq/dai-kovan"
# ... other configurations |
Good one 👍 ... small typo: const link = new ApolloLink(operation => {
const { query } = operation;
const definition = getMainDefinition(query);
const link = new ApolloLink(operation => {
const { query } = operation;
const definition = getMainDefinition(query); You have that code block duplicated ;) |
@rodrigofd Thanks, updated :) |
@in19farkt following up on your idea (love it :)) Plus, by the way, I had to add code to remove the directive at the end, otherwise the GQL server complains on it... did it happen to you ? |
I think that a 'dynamic' link should choose a link according to the directive and nothing more. The chosen link should handle authentication logic.
I had no such problems. |
@in19farkt Does the Split method described here help solve this issue as well? |
@shafqatevo for only 2 Graphql endpoints. If there are more endpoints, then the Split becomes uncomfortable. |
A working solution using multiple links in your one Apollo client. https://www.loudnoises.us/next-js-two-apollo-clients-two-graphql-data-sources-the-easy-way/ |
UPDATE in your setup const link = new RetryLink().split(
(operation) => operation.getContext().endpointA,
new HttpLink({ uri: ENDPOINT_A }),
new RetryLink().split(
(operation) => operation.getContext().endpointB,
new HttpLink({ uri: ENDPOINT_B }),
new HttpLink({ uri: DEFAULT_ENDPOINT })
)
); in your component const { data, loading, error } = useQuery(YOUR_QUERY, {
context: { endpointA: true }
}); ORIGINAL import { ApolloLink } from 'apollo-link'; // or from @apollo/client
import { getMainDefinition } from 'apollo-utilities';
const links = {
account: new HttpLink({ uri: 'http://localhost/graphql1' }),
default: new HttpLink({ uri: 'http://localhost/graphql2' })
};
const link = new ApolloLink(operation => {
const definition = getMainDefinition(operation.query);
let endpoint = 'default';
if ('operation' in definition) {
const foundDirective = definition.directives?.find(item =>
Object.keys(links).includes(item.name.value)
);
if (foundDirective) {
endpoint = foundDirective.name.value;
// remove the directive from the request
operation.query.definitions[0].directives = operation.query.definitions[0].directives.filter(
dir => dir.name.value !== endpoint
);
operation.query.loc.source.body = operation.query.loc.source.body.replace(
`@${endpoint} `,
''
);
}
}
return links[endpoint].request(operation);
}); |
Hi there, thank you all for your inputs it was very useful for us. Because we have multiple clients we created a library to handle the case of this issue. Feel free to use it or add issues 🙂 |
Thanks to you and the original solution creator in19farkt, this almost worked for me.
So my question is, does your solution remove that directive for good? Because in my implementation it does. And also if you can add an example where you can combine this solution
Thanks |
well i solve that.. using a context var.... on each useQuery or Mutation like that and in the App.js i use
|
but.. now... how.. i can send the header..!! ??? some idea....!! .. |
if you want to combine additive and directional composition with import {RestLink} from 'apollo-link-rest';
import {ApolloLink} from 'apollo-link';
import {HttpLink} from 'apollo-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
const restLink = new RestLink()
const graphql1 = new HttpLink({uri: 'http://localhost/graphql1'});
const graphql2 = new HttpLink({uri: 'http://localhost/graphql2});
const graphqlEndpoints = ApolloLink.split(
operation => operation.getContext().serviceName === 'service2',
graphql1,
graphql2,
);
const link = ApolloLink.from([restLink, graphqlEndpoints]);
const cache = new InMemoryCache();
const Client = new ApolloClient({link,cache});
export default Client; You can use "service2" endpoint as follows const {data} = useQuery(SOME_QUERY, { context: {serviceName: 'service2'} }); |
For other people coming here to find a solution - this part of the docs should solve this problem, the headers can be attached after uri in |
I am using Next.js and I followed this by adding |
@EugeneGohh That's exactly how it should be done, and how I did it as well. For headers you need to use the context link https://www.apollographql.com/docs/react/api/link/apollo-link-context/ |
@langarus Thanks for replying, I did it in this way but I got an error saying status code 401.
|
I think you should remove `.concat(endopint2)`. And rather
```
in client
link: ApolloLink.split(
(operation) => operation.getContext().clientName === "authLink",
authLink,
from([authLink, endpoint1])
from([authLink, endpoint2])
),
```
In case you want to remove the header from one endpoint in the link just
pass it directly without "from"
…On Wed, Oct 20, 2021 at 9:41 AM Eugene Goh ***@***.***> wrote:
@langarus <https://github.com/langarus> Thanks for replying, I did it in
this way but I got an error saying status code 401.
const endpoint2 = new HttpLink({
uri: "https://api.github.com/graphql",
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: `Bearer ${process.env.GITHUB_ACCESS_TOKEN}`,
},
};
}).concat(endpoint2);
const client = new ApolloClient({
ssrMode: true,
link: ApolloLink.split(
(operation) => operation.getContext().clientName === "authLink",
authLink,
endpoint1
),
cache: new InMemoryCache(),
});
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#84 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AK3JO2R7QIATPZMRDSDORMDUHZP75ANCNFSM4CAN42YA>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
It's still the same after removing. Do you have any samples how you do it? |
@langarus Hey I just fixed my problem. Thanks. If anyone needs it: I fixed it by configuring environment variables in |
One of things things that has me a little confused, is the seemingly lack of versioning recommended by GraphQL implementation articles and examples. Speaking for our team, we are already planning on making our current implementation essentially a pre release (
/graphql
) and starting with our apollo server build out atapi/v1
orgraphql/v1
.That being said, I'm interested to get thoughts on using the apollo client with multiple remote endpoints. Here are a few examples / thoughts on the api design
3rd party service + in house service
Let's say we look into the future a year or so. GraphQL has solidified around a standard (already pretty dang close!) and more and more services offer Graph endpoints along with their standard REST endpoints. So I'm building an app that pulls from my app source, and pulls from reddit for example.
Personally I am a fan of instantiating multiple clients as the two endpoints should have no overlap (if they did, your GraphQL server should be handling the linking, not the client). That also gives us the benefit of doing something like this in the future
Remote service + local service. One query / action language to rule them all
So all in all, I don't think this requires any API changes, just wanted to bring it up 👍
The text was updated successfully, but these errors were encountered: