Skip to content

Commit

Permalink
code-first stitched schemas (#11)
Browse files Browse the repository at this point in the history
* 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
yaacovCR authored Dec 28, 2020
1 parent 873751d commit df1d953
Show file tree
Hide file tree
Showing 18 changed files with 566 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ Guided examples of [Schema Stitching](https://www.graphql-tools.com/docs/stitch-
- Integrating Apollo Federation services into a stitched schema.
- Fetching and parsing Federation SDLs.

- **[Code-first schemas](./code-first-schemas)**

- Integrating schemas created with `graphql-js`, `nexus`, and `type-graphql` into a stitched schema.

### Appendices

- [What is Array Batching?](https://github.com/gmac/schema-stitching-demos/wiki/Batching-Arrays-and-Queries#what-is-array-batching)
Expand Down
91 changes: 91 additions & 0 deletions code-first-schemas/README.md
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.
69 changes: 69 additions & 0 deletions code-first-schemas/index.js
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));
14 changes: 14 additions & 0 deletions code-first-schemas/lib/make_remote_executor.js
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();
};
};
8 changes: 8 additions & 0 deletions code-first-schemas/lib/make_server.js
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`));
};
6 changes: 6 additions & 0 deletions code-first-schemas/lib/not_found_error.js
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' };
}
};
6 changes: 6 additions & 0 deletions code-first-schemas/lib/read_file_sync.js
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');
};
33 changes: 33 additions & 0 deletions code-first-schemas/package.json
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"
}
}
2 changes: 2 additions & 0 deletions code-first-schemas/services/accounts/index.js
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);
74 changes: 74 additions & 0 deletions code-first-schemas/services/accounts/schema.js
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);
2 changes: 2 additions & 0 deletions code-first-schemas/services/inventory/index.js
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);
Loading

0 comments on commit df1d953

Please sign in to comment.