-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
copy local-remote schema to graphql-upload folder
- Loading branch information
Showing
10 changed files
with
272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# Combining local and remote schemas | ||
|
||
**Watch the [chapter video](https://www.youtube.com/watch?v=8-mZqjgIdiI)** | ||
|
||
[![Combining Schemas video](../images/video-player.png)](https://www.youtube.com/watch?v=8-mZqjgIdiI) | ||
|
||
This example explores basic techniques for combining local and remote schemas together into one API. This covers most topics discussed in the [combining schemas documentation](https://www.graphql-tools.com/docs/stitch-combining-schemas). | ||
|
||
**This example demonstrates:** | ||
|
||
- Adding a locally-executable schema. | ||
- Adding a remote schema, fetched via introspection. | ||
- Adding a remote schema, fetched from a custom SDL service. | ||
- Avoiding schema conflicts using transforms. | ||
- Authorization headers. | ||
- Basic error handling. | ||
|
||
## Setup | ||
|
||
```shell | ||
cd combining-local-and-remote-schemas | ||
|
||
yarn install | ||
yarn start | ||
``` | ||
|
||
The following services are available for interactive queries: | ||
|
||
- **Stitched gateway:** http://localhost:4000/graphql | ||
- _Products subservice_: http://localhost:4001/graphql | ||
- _Storefronts subservice_: http://localhost:4002/graphql | ||
|
||
## Summary | ||
|
||
Visit the [stitched gateway](http://localhost:4000/graphql) and try running the following query: | ||
|
||
```graphql | ||
query { | ||
product(upc: "1") { | ||
upc | ||
name | ||
} | ||
rainforestProduct(upc: "2") { | ||
upc | ||
name | ||
} | ||
storefront(id: "2") { | ||
id | ||
name | ||
} | ||
errorCodes | ||
heartbeat | ||
} | ||
``` | ||
|
||
The results of this query are live-proxied from the underlying subschemas by the stitched gateway: | ||
|
||
- `product` comes from the remote Products server. This service is added into the stitched schema using introspection, i.e.: `introspectSchema` from the `@graphql-tools/wrap` package. Introspection is a tidy way to incorporate remote schemas, but be careful: not all GraphQL servers enable introspection, and those that do will not include custom directives. | ||
|
||
- `rainforestProduct` also comes from the remote Products server, although here we're pretending it's a third-party API (say, a product database named after a rainforest...). To avoid naming conflicts between our own Products schema and the Rainforest API schema, transforms are used to prefix the names of all types and fields that come from the Rainforest API. | ||
|
||
- `storefront` comes from the remote Storefronts server. This service is added to the stitched schema by querying its SDL through its own GraphQL API (very meta). While this is less conventional than introspection, it works with introspection disabled and may include custom directives. | ||
|
||
- `errorCodes` comes from a locally-executable schema running on the gateway server itself. This schema is built using `makeExecutableSchema` from the `@graphql-tools/schema` package, and then stitched directly into the combined schema. Note that this still operates as a standalone schema instance that is proxied by the top-level gateway schema. | ||
|
||
- `heartbeat` comes from type definitions and resolvers built directly into the gateway proxy layer. This is the only field in this example that returns _directly_ from the gateway schema itself; everything else delegates to an underlying subschema instance. | ||
|
||
## Authorization | ||
|
||
Authorization is relatively straightforward in a stitched schema; the only trick is that the gateway schema must pass any user authorization information (generally just an `Authorization` header) through to the underlying subservices. This is a two step process: | ||
|
||
1) Transfer authorization information from the gateway request into GraphQL context for the request: | ||
|
||
```js | ||
app.use('/graphql', graphqlHTTP((req) => ({ | ||
schema, | ||
context: { | ||
authHeader: req.headers.authorization | ||
}, | ||
}))); | ||
``` | ||
|
||
2) Add this authorization from context into the executor that builds subschema requests: | ||
|
||
```js | ||
function makeRemoteExecutor(url) { | ||
return async ({ document, variables, context }) => { | ||
const query = typeof document === 'string' ? document : print(document); | ||
const fetchResult = await fetch(url, { | ||
method: 'POST', | ||
headers: { | ||
'Authorization': context.authHeader, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ query, variables }), | ||
}); | ||
return fetchResult.json(); | ||
}; | ||
}; | ||
``` | ||
|
||
Also note that this example passes an `adminContext` into all introspection/SDL queries used to fetch remote subschemas. These requests are performed on behalf of the _gateway application_, not any specific user request. Therefore, this administrative context should provide app-to-app credentials on behalf of the gateway. | ||
|
||
## Error handling | ||
|
||
Try fetching a missing record, for example: | ||
|
||
```graphql | ||
query { | ||
product(upc: "99") { | ||
upc | ||
name | ||
} | ||
} | ||
``` | ||
|
||
You'll recieve a meaningful `NOT_FOUND` error rather than an uncontextualized null response. When building your subservices, always return meaningful errors that can flow through the stitched schema. This becomes particularily important once stitching begins to proxy records across document paths, at which time the confusion of uncontextualized failures will compound. Schema stitching errors are as good as the errors implemented by your subservices. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
const waitOn = require('wait-on'); | ||
const express = require('express'); | ||
const { graphqlHTTP } = require('express-graphql'); | ||
const { introspectSchema } = require('@graphql-tools/wrap'); | ||
const { stitchSchemas } = require('@graphql-tools/stitch'); | ||
|
||
const makeRemoteExecutor = require('./lib/make_remote_executor'); | ||
const localSchema = require('./services/local/schema'); | ||
|
||
async function makeGatewaySchema() { | ||
// Make remote executors: | ||
// these are simple functions that query a remote GraphQL API for JSON. | ||
const productsExec = makeRemoteExecutor('http://localhost:4001/graphql'); | ||
const adminContext = { authHeader: 'Bearer my-app-to-app-token' }; | ||
|
||
return stitchSchemas({ | ||
subschemas: [ | ||
{ | ||
// 1. Introspect a remote schema. Simple, but there are caveats: | ||
// - Remote server must enable introspection. | ||
// - Custom directives are not included in introspection. | ||
schema: await introspectSchema(productsExec, adminContext), | ||
executor: productsExec, | ||
}, | ||
{ | ||
// 4. Incorporate a locally-executable subschema. | ||
// No need for a remote executor! | ||
// Note that that the gateway still proxies through | ||
// to this same underlying executable schema instance. | ||
schema: localSchema | ||
} | ||
], | ||
}); | ||
} | ||
|
||
|
||
waitOn({ resources: ['tcp:4001'] }, async () => { | ||
const schema = await makeGatewaySchema(); | ||
const app = express(); | ||
app.use('/graphql', graphqlHTTP((req) => ({ | ||
schema, | ||
context: { authHeader: req.headers.authorization }, | ||
graphiql: true | ||
}))); | ||
app.listen(4000, () => console.log('gateway running at http://localhost:4000/graphql')); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const { fetch } = require('cross-fetch'); | ||
const { print } = require('graphql'); | ||
|
||
// Builds a remote schema executor function, | ||
// customize any way that you need (auth, headers, etc). | ||
// Expects to receive an object with "document" and "variable" params, | ||
// and asynchronously returns a JSON response from the remote. | ||
module.exports = function makeRemoteExecutor(url) { | ||
return async ({ document, variables, context }) => { | ||
const query = typeof document === 'string' ? document : print(document); | ||
const fetchResult = await fetch(url, { | ||
method: 'POST', | ||
headers: { | ||
'Authorization': context.authHeader, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ query, variables }), | ||
}); | ||
return fetchResult.json(); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = class NotFoundError extends Error { | ||
constructor(message) { | ||
super(message || 'Record not found'); | ||
this.extensions = { code: 'NOT_FOUND' }; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
|
||
module.exports = function readFileSync(dir, filename) { | ||
return fs.readFileSync(path.join(dir, filename), 'utf8'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"name": "combining-local-and-remote-schemas", | ||
"version": "0.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"start-products": "nodemon --watch services/products services/products/index.js", | ||
"start-gateway": "nodemon index.js", | ||
"start": "concurrently \"yarn:start-*\"" | ||
}, | ||
"dependencies": { | ||
"@graphql-tools/schema": "^7.0.0", | ||
"@graphql-tools/stitch": "^7.0.4", | ||
"@graphql-tools/wrap": "^7.0.1", | ||
"concurrently": "^5.3.0", | ||
"cross-fetch": "^3.0.6", | ||
"express": "^4.17.1", | ||
"express-graphql": "^0.12.0", | ||
"graphql": "^15.4.0", | ||
"nodemon": "^2.0.6", | ||
"wait-on": "^5.2.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
const { makeExecutableSchema } = require('@graphql-tools/schema'); | ||
|
||
module.exports = makeExecutableSchema({ | ||
typeDefs: ` | ||
type Query { | ||
errorCodes: [String!]! | ||
} | ||
`, | ||
resolvers: { | ||
Query: { | ||
errorCodes: () => [ | ||
'NOT_FOUND', | ||
'GRAPHQL_PARSE_FAILED', | ||
'GRAPHQL_VALIDATION_FAILED', | ||
] | ||
} | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
const express = require('express'); | ||
const { graphqlHTTP } = require('express-graphql'); | ||
const schema = require('./schema'); | ||
|
||
const app = express(); | ||
app.use('/graphql', graphqlHTTP({ schema, graphiql: true })); | ||
app.listen(4001, () => console.log('products running at http://localhost:4001/graphql')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
type Product { | ||
name: String! | ||
price: Float! | ||
upc: ID! | ||
} | ||
|
||
type Query { | ||
product(upc: ID!): Product | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const { makeExecutableSchema } = require('@graphql-tools/schema'); | ||
const NotFoundError = require('../../lib/not_found_error'); | ||
const readFileSync = require('../../lib/read_file_sync'); | ||
const typeDefs = readFileSync(__dirname, 'schema.graphql'); | ||
|
||
// data fixtures | ||
const products = [ | ||
{ upc: '1', name: 'Cookbook', price: 15.99 }, | ||
{ upc: '2', name: 'Toothbrush', price: 3.99 }, | ||
]; | ||
|
||
module.exports = makeExecutableSchema({ | ||
typeDefs, | ||
resolvers: { | ||
Query: { | ||
product: (root, { upc }) => products.find(p => p.upc === upc) || new NotFoundError() | ||
} | ||
} | ||
}); |