Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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