-
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.
* initial commit accounts is now world's first code-first stitched schema using graphql-js to do: = convert a service to TypeGraphQL = convert a service to nexus = convert a service to... = simplify type merging configuration to only use simple keys -- as this example is not meant to showcase the more coplex options = fix the awkwardness with `{ stitchingDirectives: directive } = stitchingDirectives()` * fix name clash based on latest alpha * rename folder * upgrade dependencies * use latest graphql-tools alpha export names * nexus supprt * remove copied text * add TypeGraphQL example * update README * integrate into table of contents * fix notes * add missing dep * update deps * fix TypeGraphQL example * formatting * simplify * finesse
- Loading branch information
Showing
18 changed files
with
566 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
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,91 @@ | ||
# Stitching Directives with Code-First Schemas | ||
|
||
This example demonstrates the use of stitching directives to configure type merging, similar to the prior example, but uses code-first schemas instead of SDL. | ||
|
||
The `@graphql-tools/stitching-directives` package provides importable directives definitions that can be used to annotate types and fields within subschemas, a validator to ensure the directives are used appropriately, and a configuration transformer that can be used on the gateway to convert the subschema directives into explicit configuration setting. | ||
|
||
It also provides pre-built directives to be used with code-first schemas that do not parse SDL. The validator is configured to read directives from GraphQL entity extensions, which actually take priority when present over the SDL. | ||
|
||
The `@graphql-tools/utils` package also exports a function that can print these "directives within extensions" as actual directives that can be then exposed via subservice to the gateway. | ||
|
||
Note: the service setup in this example is based on the [official demonstration repository](https://github.com/apollographql/federation-demo) for | ||
[Apollo Federation](https://www.apollographql.com/docs/federation/). | ||
|
||
**This example demonstrates:** | ||
|
||
- Use of the @key, @computed and @merge "directives within extensions" to specify type merging configuration. | ||
|
||
## Setup | ||
|
||
```shell | ||
cd code-first-schemas | ||
|
||
yarn install | ||
yarn start-services | ||
``` | ||
|
||
The following services are available for interactive queries: | ||
|
||
- **Stitched gateway:** http://localhost:4000/graphql | ||
- _Accounts subservice_: http://localhost:4001/graphql | ||
- _Inventory subservice_: http://localhost:4002/graphql | ||
- _Products subservice_: http://localhost:4003/graphql | ||
- _Reviews subservice_: http://localhost:4004/graphql | ||
|
||
## Summary | ||
|
||
First, try a query that includes data from all services: | ||
|
||
```graphql | ||
query { | ||
products(upcs: [1, 2]) { | ||
name | ||
price wit | ||
weight | ||
inStock | ||
shippingEstimate | ||
reviews { | ||
id | ||
body | ||
author { | ||
name | ||
username | ||
totalReviews | ||
} | ||
product { | ||
name | ||
price | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Neat, it works! All those merges were configured through schema annotations within schemas! | ||
|
||
### Accounts subservice | ||
|
||
The Accounts subservice showcases how schemas created with vanilla `graphql-js` can also utilize stitching directives to achieve the benefits of colocating types and their merge configuration, including support for hot-reloading: | ||
|
||
- _Directive usages_: implemented as "directives within extensions," i.e. following the Gatsby/graphql-compose convention of embedding third party directives under the `directives` key of each GraphQL entity's `extensions` property. | ||
- _Directive declarations_: directly added to the schema by using the compiled directives exported by the `@graphql-tools/stitching-directives` package. | ||
|
||
### Inventory subservice | ||
|
||
The Inventory subservice demonstrates using stitching directives with a schema created using the `nexus` library: | ||
|
||
- _Directive usages_: implemented as "directives within extensions," i.e. following the Gatsby/graphql-compose convention of embedding third party directives under the `directives` key of each GraphQL entity's `extensions` property. | ||
- _Directive declarations_: `nexus` does not yet support passing in built `graph-js` `GraphQLDirective` objects, but you can easily create a new schema from the `nexus` schema programatically (using `new GraphQLSchema({ ...originalSchema.toConfig(), directives: [...originalSchema.getDirectives(), ...allStitchingDirectives] })`. | ||
|
||
### Products subservice | ||
|
||
The Products subservice shows how `TypeGraphQL` can easily implement third party directives including stitching directives. | ||
|
||
- _Directive usages_: implemented using the @Directive decorator syntax, TypeGraphQL's method of supporting third party directives within its code-first schema. | ||
- _Directive declarations_: not strictly required -- TypeGraphQL does not validate the directive usage SDL, and creates actual directives under the hood, as if they were created with SDL, so directive declarations are actually not required. This makes setup a bit easier, at the cost of skipping a potentially helpful validation step. | ||
|
||
# Reviews subservice | ||
The Reviews subservice is available for comparison to remind us of how `makeExecutableSchema` utilizes directives with SDL. | ||
|
||
- _Directive usages_: implemented using directives within actual SDL. | ||
- _Directive declarations_: directive type definitions are imported from the `@graphql-tools/stitching-directives` package. |
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,69 @@ | ||
const { stitchSchemas } = require('@graphql-tools/stitch'); | ||
const { stitchingDirectives } = require('@graphql-tools/stitching-directives'); | ||
const { buildSchema } = require('graphql'); | ||
const makeServer = require('./lib/make_server'); | ||
const makeRemoteExecutor = require('./lib/make_remote_executor'); | ||
|
||
const { stitchingDirectivesTransformer } = stitchingDirectives(); | ||
|
||
async function makeGatewaySchema() { | ||
const accountsExec = makeRemoteExecutor('http://localhost:4001/graphql'); | ||
const inventoryExec = makeRemoteExecutor('http://localhost:4002/graphql'); | ||
const productsExec = makeRemoteExecutor('http://localhost:4003/graphql'); | ||
const reviewsExec = makeRemoteExecutor('http://localhost:4004/graphql'); | ||
|
||
return stitchSchemas({ | ||
subschemaConfigTransforms: [stitchingDirectivesTransformer], | ||
subschemas: [ | ||
{ | ||
schema: await fetchRemoteSchema(accountsExec), | ||
executor: accountsExec, | ||
}, | ||
{ | ||
schema: await fetchRemoteSchema(inventoryExec), | ||
executor: inventoryExec, | ||
}, | ||
{ | ||
schema: await fetchRemoteSchema(productsExec), | ||
executor: productsExec, | ||
}, | ||
{ | ||
schema: await fetchRemoteSchema(reviewsExec), | ||
executor: reviewsExec, | ||
} | ||
] | ||
}); | ||
} | ||
|
||
// fetch remote schemas with a retry loop | ||
// (allows the gateway to wait for all services to startup) | ||
async function fetchRemoteSchema(executor) { | ||
return new Promise((resolve, reject) => { | ||
async function next(attempt=1) { | ||
try { | ||
const { data } = await executor({ document: '{ _sdl }' }); | ||
resolve(buildSchema(data._sdl)); | ||
// Or: | ||
// | ||
// resolve(buildSchema(data._sdl, { assumeValidSDL: true })); | ||
// | ||
// `assumeValidSDL: true` is necessary if a code-first schema implements directive | ||
// usage, either directly or by extensions, but not addition of actual custom | ||
// directives. Alternatively, a new schema with the directives could be created | ||
// from the nexus schema using: | ||
// | ||
// const newSchema = new GraphQLSchema({ | ||
// ...originalSchema.toConfig(), | ||
// directives: [...originalSchema.getDirectives(), ...allStitchingDirectives] | ||
// }); | ||
// | ||
} catch (err) { | ||
if (attempt >= 10) reject(err); | ||
setTimeout(() => next(attempt+1), 300); | ||
} | ||
} | ||
next(); | ||
}); | ||
} | ||
|
||
makeGatewaySchema().then(schema => makeServer(schema, 'gateway', 4000)); |
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,14 @@ | ||
const { fetch } = require('cross-fetch'); | ||
const { print } = require('graphql'); | ||
|
||
module.exports = function makeRemoteExecutor(url) { | ||
return async ({ document, variables }) => { | ||
const query = typeof document === 'string' ? document : print(document); | ||
const fetchResult = await fetch(url, { | ||
method: 'POST', | ||
headers: { '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,8 @@ | ||
const express = require('express'); | ||
const { graphqlHTTP } = require('express-graphql'); | ||
|
||
module.exports = function makeServer(schema, name, port=4000) { | ||
const app = express(); | ||
app.use('/graphql', graphqlHTTP({ schema, graphiql: true })); | ||
app.listen(port, () => console.log(`${name} running at http://localhost:${port}/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,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,33 @@ | ||
{ | ||
"name": "stitching-directives-sdl", | ||
"version": "0.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"start-service-accounts": "nodemon -e js,graphql services/accounts/index.js", | ||
"start-service-inventory": "nodemon -e js,graphql services/inventory/index.js", | ||
"start-service-products": "nodemon --watch services/products/**/*.ts --exec ts-node services/products/index.ts", | ||
"start-service-reviews": "nodemon -e js,graphql services/reviews/index.js", | ||
"start-service-gateway": "nodemon -e js,graphql index.js", | ||
"start-services": "concurrently \"yarn:start-service-*\"" | ||
}, | ||
"dependencies": { | ||
"@graphql-tools/schema": "^7.1.2", | ||
"@graphql-tools/stitch": "^7.1.6", | ||
"@graphql-tools/stitching-directives": "^1.1.0", | ||
"@graphql-tools/utils": "^7.2.3", | ||
"@types/node": "^14.14.16", | ||
"class-validator": "^0.12.2", | ||
"concurrently": "^5.3.0", | ||
"cross-fetch": "^3.0.6", | ||
"express": "^4.17.1", | ||
"express-graphql": "^0.12.0", | ||
"graphql": "^15.4.0", | ||
"nexus": "^1.0.0", | ||
"nodemon": "^2.0.6", | ||
"reflect-metadata": "^0.1.13", | ||
"ts-node": "^9.1.1", | ||
"type-graphql": "^1.1.1", | ||
"typescript": "^4.1.3" | ||
} | ||
} |
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,2 @@ | ||
const makeServer = require('../../lib/make_server'); | ||
makeServer(require('./schema'), 'accounts', 4001); |
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,74 @@ | ||
const { | ||
GraphQLScalarType, | ||
GraphQLSchema, | ||
GraphQLObjectType, | ||
GraphQLList, | ||
GraphQLNonNull, | ||
GraphQLString, | ||
GraphQLID, | ||
specifiedDirectives | ||
} = require('graphql'); | ||
const { stitchingDirectives } = require('@graphql-tools/stitching-directives'); | ||
const { printSchemaWithDirectives } = require('@graphql-tools/utils'); | ||
const NotFoundError = require('../../lib/not_found_error'); | ||
|
||
const { allStitchingDirectives, stitchingDirectivesValidator } = stitchingDirectives(); | ||
|
||
const users = [ | ||
{ id: '1', name: 'Ada Lovelace', username: '@ada' }, | ||
{ id: '2', name: 'Alan Turing', username: '@complete' }, | ||
]; | ||
|
||
const accountsSchemaTypes = Object.create(null); | ||
|
||
accountsSchemaTypes._Key = new GraphQLScalarType({ | ||
name: '_Key', | ||
}); | ||
accountsSchemaTypes.Query = new GraphQLObjectType({ | ||
name: 'Query', | ||
fields: () => ({ | ||
me: { | ||
type: accountsSchemaTypes.User, | ||
resolve: () => users[0], | ||
}, | ||
user: { | ||
type: accountsSchemaTypes.User, | ||
args: { | ||
id: { | ||
type: new GraphQLNonNull(GraphQLID), | ||
}, | ||
}, | ||
resolve: (_root, { id }) => users.find(user => user.id === id) || new NotFoundError(), | ||
extensions: { directives: { merge: { keyField: 'id' } } }, | ||
}, | ||
_sdl: { | ||
type: new GraphQLNonNull(GraphQLString), | ||
resolve(_root, _args, _context, info) { | ||
return printSchemaWithDirectives(info.schema); | ||
} | ||
}, | ||
}), | ||
}); | ||
|
||
accountsSchemaTypes.User = new GraphQLObjectType({ | ||
name: 'User', | ||
fields: () => ({ | ||
id: { type: GraphQLID }, | ||
name: { type: GraphQLString }, | ||
username: { type: GraphQLString }, | ||
}), | ||
extensions: { | ||
directives: { | ||
key: { | ||
selectionSet: '{ id }', | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const accountsSchema = new GraphQLSchema({ | ||
query: accountsSchemaTypes.Query, | ||
directives: [...specifiedDirectives, ...allStitchingDirectives], | ||
}); | ||
|
||
module.exports = stitchingDirectivesValidator(accountsSchema); |
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,2 @@ | ||
const makeServer = require('../../lib/make_server'); | ||
makeServer(require('./schema'), 'inventory', 4002); |
Oops, something went wrong.