├── .circleci └── config.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── deep-dive │ └── README.md ├── getting-started │ ├── README.md │ ├── package.json │ ├── server.ts │ └── tsconfig.json └── multiple-schemas │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── tslint.json ├── package.json ├── src ├── SuperGraph.ts ├── helpers.ts ├── index.ts ├── middlewares │ ├── base.spec.ts │ ├── base.ts │ ├── generateSchema.ts │ ├── index.ts │ ├── remoteSchema.ts │ ├── resolver.ts │ ├── schema.spec.ts │ ├── schema.ts │ ├── serve.ts │ ├── transform.ts │ └── use.ts ├── types.ts └── typings │ ├── custom.d.ts │ ├── graphql-add-middleware.d.ts │ └── https-proxy-agent.ts ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:latest 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/supergraph 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: 30 | name: install dependencies 31 | command: yarn install 32 | 33 | - save_cache: 34 | paths: 35 | - node_modules 36 | key: v1-dependencies-{{ checksum "package.json" }} 37 | - run: 38 | name: run tests and report 39 | command: 'npx nyc -r html --report-dir reports/coverage/html mocha ~/supergraph/dist/**/*.spec.js --reporter=xunit --reporter-options output=reports/test-results.xml' 40 | - run: 41 | name: generate test coverage report 42 | command: 'npx nyc report --reporter=text-lcov > reports/coverage/coverage.lcov' 43 | - store_test_results: 44 | path: reports 45 | - store_artifacts: 46 | path: reports/test-results.xml 47 | prefix: tests 48 | - store_artifacts: 49 | path: reports/coverage 50 | prefix: coverage 51 | - run: 52 | name: upload code coverage to codecov 53 | command: bash <(curl -s https://codecov.io/bash) 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn.lock 4 | package-lock.json 5 | .vscode 6 | examples 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/ 3 | !dist/* 4 | !package.json 5 | !*.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kim Brandwijk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuperGraph 2 | 3 | [![npm](https://img.shields.io/npm/v/supergraph.svg?style=for-the-badge)]() 4 | [![CircleCI](https://img.shields.io/circleci/project/github/supergraphql/supergraph.svg?style=for-the-badge)]() 5 | [![Codecov](https://img.shields.io/codecov/c/github/supergraphql/supergraph.svg?style=for-the-badge)](https://codecov.io/gh/supergraphql/supergraph) 6 | 7 | 8 | **SuperGraph** is a **GraphQL Server Framework**. 9 | It is inspired by Koa, built on Apollo Server, and turns your GraphQL endpoint into a Koa-like application, with support for context, middleware, and many other features. 10 | 11 | It is great for setting up an API Gateway on top of existing GraphQL endpoint, applying concepts like **remote schemas** and **schema stitching**. But it also makes it very easy to set up a GraphQL Server **from scratch**. 12 | 13 | SuperGraph is implemented as a set of Express middlewares, so you can build a GraphQL Server as easily as a regular web server! 14 | 15 | ## Concepts 16 | 17 | SuperGraph bridges the gap between commonly used patterns for HTTP middleware frameworks like Koa and Express, and GraphQL endpoints. 18 | A GraphQL server is composed of three components: 19 | - GraphQL schemas 20 | - Resolvers 21 | - Context 22 | 23 | SuperGraph uses these same three components: 24 | - GraphQL schemas define the **routes** of our server 25 | - Resolvers define the **implementation** of these routes 26 | - Application middleware is used to construct the **context** passed to every resolver, and router middleware is used to add **common functionality** to the resolvers 27 | 28 | ## Installation 29 | SuperGraph requires **node v7.6.0** or higher for ES2015 and async function support. Following common practice, it has `apollo-link` and `graphql` specified as peer dependencies, so make sure you install those too. 30 | ```bash 31 | $ yarn add supergraph apollo-link graphql 32 | ``` 33 | 34 | ## Examples 35 | 36 | ### [Getting started](./examples/getting-started#readme) 37 | Set up your first GraphQL Server in minutes using Express and SuperGraph. 38 | 39 | ### [Deep Dive](./examples/deep-dive#readme) 40 | SuperGraph makes it easy to set up a GraphQL Server from scratch. But this example really takes it to the next level by combining two existing GraphQL endpoints, link them together, and apply some middleware on top. 41 | 42 | ## Documentation 43 | 44 | ### Express Request 45 | 46 | SuperGraph add a `supergraph` object to the Express Request context, with the following structure. For normal use cases, you never have to modify anything directly. 47 | - `req.supergraph.context.schemas` will contain all GraphQL schemas 48 | - `req.supergraph.context.resolvers` will contain all GraphQL resolver functions 49 | - `req.supergraph.context.middlewares` will contain all GraphQL middleware functions 50 | - `req.supergraph.context.mergedSchema` will contain the final schema used for your endpoint 51 | 52 | ### `schema({name?: string, schema: GraphQLSchema | string})` or 53 | ### `schema(schema: GraphQLSchema | string)` 54 | 55 | Adds a GraphQL schema to the SuperGraph application. This can either be a full GraphQLSchema or a Type definition string. Optionally you can specify a name for the schema, so you can reference it anywhere using `context.schemas.schemaName`. This is actually a convenient shorthand for the `schema` SuperGraph middleware function: `use(schema(...))` 56 | 57 |

remoteSchema({
 58 |   name?: string,
 59 |   uri?: string,
 60 |   introspectionSchema?: GraphQLSchema,
 61 |   authenticationToken?: (context) => string},
 62 |   forwardHeaders?: boolean | Array<string>
 63 | })
64 | remoteSchema({ 65 | name?: string, 66 | link?: ApolloLink, 67 | introspectionSchema?: GraphQLSchema, 68 | authenticationToken?: (context) => string}, 69 | forwardHeaders?: boolean | Array 70 | })

71 | 72 | Adds a schema for a remote GraphQL endpoint to the SuperGraph Application. You can either specify a URL for the endpoint, or pass in an existing `ApolloLink`. 73 | You can optionally pass in an introspectionSchema. If you don't, SuperGraph will run the introspection query against your endpoint to retrieve the schema. 74 | You can also optionally pass in a function that retrieves the authenticationToken from the GraphQL context. It will be added as Bearer token to the Authorization header for this remote endpoint. For more complex scenarios, use the possibility to pass in your own `ApolloLink` definition. 75 | 76 | ### `use(path: '...', fn: async (event, next) => {...}`) 77 | 78 | Specify a router middleware for a certain GraphQL path. You can specify multiple middlewares for the same path, and each middleware can specify actions before and after the next middleware or resolver runs (like Koa). The basic syntax is: 79 | ```ts 80 | (event, next) => { 81 | // Do something before 82 | const result = await next() 83 | // Do something after 84 | return result 85 | } 86 | ``` 87 | The `event` object contains the following properties: 88 | - **`parent`**, **`args`**, **`context`** and **`info`**. These are the common resolver parameters. 89 | - **`addFields`**. Helper method to add fields to the resolver (see below). 90 | 91 | ### `resolve(path: string, fn: async (event) => {...}` 92 | 93 | Specify a regular resolver for a GraphQL path. This type of resolver is used for fields that are not part of a remote schema. You can only specify one resolver per GraphQL path. The `event` object contains the same properties as above, and also: 94 | 95 |

resolve(path: string, resolver: {
 96 |   fragment?: string
 97 |   resolve: async (event) => {...}
 98 |   __resolveType?: async (event) => {...}
 99 | })

100 | 101 | Specify a merge (schema stitching) resolver for a GraphQL path. You can only specify one resolver per GraphQL path. The `event` object contains the same properties as above, and also: 102 | - **`addTypenameField()`**. Helper method. See below 103 | - **`delegate()`**. Helper method to delegate execution to an existing query or mutation (useful for schema stitching). See below 104 | - **`delegateQuery()`**. Helper method to execute a custom query or mutation against your underlying schema (useful for schema stitching). See below 105 | 106 | ### `await serve({serverOptions})` 107 | 108 | This method exposes the Express middleware for your SuperGraph application. You add it to your GraphQL Express route (`/graphql`). It accepts the following Apollo Server serverOptions, that it will pass through to Apollo Server: 109 | * **rootValue**: the value passed to the first resolve function 110 | * **formatError**: a function to apply to every error before sending the response to clients 111 | * **validationRules**: additional GraphQL validation rules to be applied to client-specified queries 112 | * **formatParams**: a function applied for each query in a batch to format parameters before execution 113 | * **formatResponse**: a function applied to each response after execution 114 | * **tracing**: when set to true, collect and expose trace data in the [Apollo Tracing format](https://github.com/apollographql/apollo-tracing) 115 | 116 | 117 | ### Helper methods 118 | 119 | SuperGraph comes with a number of helper methods that you can use inside your resolvers: 120 | 121 | #### `addFields(fields: [FieldNode | string] | FieldNode | string)` 122 | 123 | Helper method to add one or more fields to your query. This is useful when you need a specific field to be part of the query result for further processing, even if the user doesn't specify it in their query. 124 | 125 | #### `addTypenameField()` 126 | 127 | Helper method to add the `__typename` field to your query fields. This is useful for resolvers for interface and union fields, that require the `__typename` field for Type resolving. This is an explicit shorthand for `addFields('__typename')`. It is a separate function because it is to be expected that `graphql-tools` will support this out of the box soon, so you don't have to specify this field manually anymore. 128 | 129 | #### `delegate(operationType: 'query' | 'mutation', operationName: '...', args?: {...})` 130 | 131 | This is a helper method for `mergeInfo.delegate`, and one of the most used methods when defining resolvers for **schema stitching**. It delegates the execution to the query or mutation with the specified operationName. Optionally, you can specify query arguments. If you don't specify arguments, the resolver arguments will be injected automatically. 132 | 133 | #### `delegateQuery(query: string, args?: {...})` 134 | 135 | This is a helper method that works like `mergeInfo.delegate`, but instead of specifying an operationName, you can define a custom query. This is very useful for executing a query that is unrelated to the resolver (for example, retrieve user data). As with `delegate`, arguments are optional, and will be injected automatically. 136 | 137 | ## Support for other server frameworks 138 | 139 | Support for other server frameworks (Hapi, Koa, Restify, Lambda, Micro and Azure Functions) will be released soon! 140 | 141 | ## Additional middlewares 142 | 143 | Additional useful SuperGraph application and router middlewares (for logging, authentication, mocking, etc.) will be released soon as a separate package! 144 | 145 | ## Contributing 146 | 147 | If you run into any issue using SuperGraph, or if you have a feature request, please open an [issue](https://github.com/supergraphql/supergraph/issues/new). 148 | -------------------------------------------------------------------------------- /examples/deep-dive/README.md: -------------------------------------------------------------------------------- 1 | # Deep Dive 2 | 3 | _Coming soon_ 4 | -------------------------------------------------------------------------------- /examples/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | This example demonstrates the basics of setting up your own GraphQL server. If you are already familiar with the concepts involved, you can skip to the **Deep Dive example** [here](../deep-dive#readme). 4 | 5 | ## Running the example 6 | To run the finished example, run: 7 | ```bash 8 | $ yarn 9 | $ yarn start 10 | ``` 11 | 12 | ## Express Server 13 | Let's start with a basic Express server: 14 | ```ts 15 | import * as express from 'express' 16 | 17 | async function run() { 18 | 19 | const app = express() 20 | 21 | app.listen(3000) 22 | } 23 | 24 | run() 25 | ``` 26 | ## Express Routes 27 | Now, let's create a route for our GraphQL server: 28 | ```diff 29 | import * as express from 'express' 30 | +import { expressPlayground } from 'graphql-playground-middleware' 31 | 32 | async function run() { 33 | 34 | const app = express() 35 | 36 | + app.use('/graphql', express.json()) 37 | 38 | + app.use('/playground', expressPlayground({ endpoint: '/graphql' })) 39 | 40 | - app.listen(3000) 41 | + app.listen(3000, () => console.log('Server running. Open http://localhost:3000/playground to run queries.')) 42 | } 43 | 44 | run() 45 | ``` 46 | We have added an endpoint `/graphql` where our GraphQL server is going to live. We also created an endpoint `/playground` using the excellent [`graphql-playground-middleware`](https://github.com/graphcool/graphql-playground) package from [Graphcool](https://graph.cool), a GraphQL playground on steroids, so we can easily test our queries later on. 47 | 48 | ## Qewl! 49 | Now it's time to set up our Qewl middleware! We start by setting up an Express route, and defining the schema for our GraphQL server. 50 | ```diff 51 | import * as express from 'express' 52 | import * as cors from 'cors' 53 | import * as bodyParser from 'body-parser' 54 | import { expressPlayground } from 'graphql-playground-middleware' 55 | +import { schema } from 'qewl' 56 | 57 | async function run() { 58 | 59 | const app = express() 60 | 61 | + const grapqhl = express.Router() 62 | + graphql.use( 63 | + schema(` 64 | + type HelloPayload { 65 | + message: String 66 | + } 67 | + 68 | + type Query { 69 | + hello: HelloPayload 70 | + } 71 | + `) 72 | + ) 73 | 74 | - app.use('/graphql', express.json()) 75 | + app.use('/graphql', express.json(), graphql, await serve()) 76 | 77 | app.use('/playground', expressPlayground({ endpoint: '/graphql' })) 78 | 79 | app.listen(3000, () => console.log('Server running. Open http://localhost:3000/playground to run queries.')) 80 | } 81 | 82 | run() 83 | ``` 84 | Congratulations, you have created your GraphQL server! You can now visit [http://localhost:3000/playground](http://localhost:3000/playground) and run your first query. 85 | 86 | ![first-attempt](https://user-images.githubusercontent.com/852069/32813123-dc222878-c9a8-11e7-9e70-dd078c64d5e9.png) 87 | 88 | As you see, the query doesn't return any data yet. We have defined our GraphQL schema, but we haven't defined any **implementation** for the routes defined in our schema. 89 | 90 | ## Qewl resolver 91 | So let's get back to the code, and add our **resolver**. 92 | ```diff 93 | import * as express from 'express' 94 | import * as cors from 'cors' 95 | import * as bodyParser from 'body-parser' 96 | import { expressPlayground } from 'graphql-playground-middleware' 97 | -import { schema } from 'qewl' 98 | +import { schema, resolve } from 'qewl' 99 | 100 | async function run() { 101 | 102 | const app = express() 103 | 104 | const grapqhl = express.Router() 105 | graphql.use( 106 | schema(` 107 | type HelloPayload { 108 | message: String 109 | } 110 | 111 | type Query { 112 | hello: HelloPayload 113 | } 114 | `) 115 | ) 116 | 117 | + graphql.use( 118 | + resolve('Query.hello', async (event) => { 119 | + return { message: `Hello ${event.args.name}!` } 120 | + }) 121 | + ) 122 | 123 | app.use('/graphql', express.json(), graphql, await serve()) 124 | 125 | app.use('/playground', expressPlayground({ endpoint: '/graphql' })) 126 | 127 | app.listen(3000, () => console.log('Server running. Open http://localhost:3000/playground to run queries.')) 128 | } 129 | 130 | run() 131 | ``` 132 | We have defined a resolver for our GraphQL route `Query.hello`. Let's run that query again... 133 | 134 | ![second-attempt](https://user-images.githubusercontent.com/852069/32813445-816489ba-c9aa-11e7-8474-d3ab42b2fe14.png) 135 | 136 | Now, that looks a lot better. With just a few lines of codes, you have created a GraphQL Server from scratch, defining the schema and resolver and running the server as Express middleware. 137 | -------------------------------------------------------------------------------- /examples/getting-started/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "ts-node ./server.ts" 4 | }, 5 | "dependencies": { 6 | "apollo-link": "^1.0.3", 7 | "express": "^4.16.2", 8 | "graphql": "^0.11.7", 9 | "graphql-playground-middleware": "^1.1.2", 10 | "qewl": "^0.2.2" 11 | }, 12 | "devDependencies": { 13 | "@types/express": "^4.0.39", 14 | "ts-node": "^3.3.0", 15 | "typescript": "^2.6.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/getting-started/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | import * as cors from 'cors' 3 | import * as bodyParser from 'body-parser' 4 | import { expressPlayground } from 'graphql-playground-middleware' 5 | import { schema, resolve } from 'qewl' 6 | 7 | async function run() { 8 | 9 | const app = express() 10 | 11 | const grapqhl = express.Router() 12 | graphql.use( 13 | schema(` 14 | type HelloPayload { 15 | message: String 16 | } 17 | 18 | type Query { 19 | hello: HelloPayload 20 | } 21 | `) 22 | ) 23 | 24 | graphql.use( 25 | resolve('Query.hello', async (event) => { 26 | return { message: `Hello ${event.args.name}!` } 27 | }) 28 | ) 29 | 30 | app.use('/graphql', express.json(), graphql, await serve()) 31 | 32 | app.use('/playground', expressPlayground({ endpoint: '/graphql' })) 33 | 34 | app.listen(3000, () => console.log('Server running. Open http://localhost:3000/playground to run queries.')) 35 | } 36 | 37 | run() 38 | -------------------------------------------------------------------------------- /examples/getting-started/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "lib": ["es7", "dom", "esnext.asynciterable"], 5 | "module": "commonjs", 6 | "target": "es5", 7 | "noImplicitAny": true, 8 | "suppressImplicitAnyIndexErrors": true, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "noUnusedLocals": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "rootDir": ".", 15 | "outDir": "./dist", 16 | "removeComments": false 17 | }, 18 | "exclude": ["node_modules", "dist"] 19 | } -------------------------------------------------------------------------------- /examples/multiple-schemas/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .env 3 | yarn.lock 4 | yarn-error.log 5 | .vscode 6 | -------------------------------------------------------------------------------- /examples/multiple-schemas/index.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | import * as express from 'express' 4 | import expressPlayground from 'graphql-playground-middleware-express' 5 | import { remoteSchema, schema, resolver, serve, use, transform } from 'qewl' 6 | 7 | async function run() { 8 | const app = express() 9 | const graphql = express.Router() 10 | 11 | // Schemas 12 | graphql.use( 13 | remoteSchema({ 14 | uri: 15 | process.env.GRAPHCOOL_ADDRESS_ENDPOINT || 'https://api.graph.cool/simple/v1/cj97mysum1jyi01363osr460n' 16 | }), 17 | 18 | remoteSchema({ 19 | uri: 20 | process.env.GRAPHCOOL_WEATHER_ENDPOINT || 'https://api.graph.cool/simple/v1/cj97mrhgb1jta01369lzb0tam' 21 | }), 22 | 23 | schema(` 24 | extend type Address { 25 | weather: WeatherPayload 26 | } 27 | 28 | extend type WeatherPayload { 29 | temp(unit: UnitEnum): Float 30 | } 31 | 32 | enum UnitEnum { 33 | Celcius, 34 | Fahrenheit 35 | }`) 36 | ) 37 | 38 | // Resolvers 39 | graphql.use( 40 | resolver('Address.weather', { 41 | fragment: `fragment AddressFragment on Address { city }`, 42 | resolve: event => { 43 | const { city } = event.parent 44 | return event.delegate('query', 'getWeatherByCity', { city }) 45 | } 46 | }), 47 | 48 | resolver('WeatherPayload.temp', { 49 | fragment: `fragment WeatherPayloadFragment on WeatherPayload { temperature }`, 50 | resolve: event => { 51 | const { temperature } = event.parent 52 | switch (event.args.unit) { 53 | case 'Fahrenheit': 54 | return temperature * 1.8 + 32 55 | default: 56 | return temperature 57 | } 58 | } 59 | }) 60 | ) 61 | 62 | // Middleware 63 | graphql.use( 64 | use('User.name', async (event, next) => { 65 | if (!event.context.headers.scopes || ''.includes('read:username')) { 66 | throw new Error('Insufficient permissions') 67 | } else { 68 | return await next() 69 | } 70 | }) 71 | ) 72 | 73 | // Define final schema 74 | graphql.use( 75 | transform(` 76 | type Query { 77 | getWeatherByCity(city: String): WeatherPayload 78 | # other queries are removed here 79 | } 80 | 81 | # Mutation type is removed entirely, because it isn't specified 82 | 83 | type WeatherPayload { 84 | temp(unit: UnitEnum): Float 85 | # other fields are removed here 86 | } 87 | 88 | enum UnitEnum { 89 | Celcius 90 | # other enum value is removed here 91 | }`) 92 | ) 93 | 94 | app.use('/graphql', express.json(), graphql, await serve()) 95 | app.use('/playground', expressPlayground({ endpoint: '/graphql' })) 96 | 97 | app.listen(3000, () => console.log('Server running. Open http://localhost:3000/playground to run queries.')) 98 | } 99 | 100 | run().catch(console.error.bind(console)) 101 | -------------------------------------------------------------------------------- /examples/multiple-schemas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "nodemon --ext ts --exec ts-node index.ts" 4 | }, 5 | "dependencies": { 6 | "apollo-link": "^1.0.3", 7 | "dotenv": "^4.0.0", 8 | "express": "^4.16.2", 9 | "graphql": "^0.11.7", 10 | "graphql-playground-middleware-express": "^1.1.4", 11 | "qewl": "^0.2.7" 12 | }, 13 | "devDependencies": { 14 | "@types/express": "^4.0.39", 15 | "nodemon": "^1.12.1", 16 | "ts-node": "^3.3.0", 17 | "typescript": "^2.6.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/multiple-schemas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "sourceMap": true, 6 | "noImplicitAny": false, 7 | "noImplicitReturns": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "strictNullChecks": true, 10 | "noUnusedLocals": true, 11 | "lib": [ 12 | "es2017" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/multiple-schemas/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "debug", 40 | "info", 41 | "time", 42 | "timeEnd", 43 | "trace" 44 | ], 45 | "no-construct": true, 46 | "no-parameter-properties": true, 47 | "no-debugger": true, 48 | "no-duplicate-variable": true, 49 | "no-empty": [true, "allow-empty-catch"], 50 | "no-eval": true, 51 | "no-inferrable-types": false, 52 | "no-internal-module": true, 53 | "no-null-keyword": false, 54 | "no-require-imports": false, 55 | "no-shadowed-variable": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-use-before-declare": false, 60 | "no-var-keyword": true, 61 | "object-literal-sort-keys": false, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-catch", 66 | "check-else", 67 | "check-finally", 68 | "check-whitespace" 69 | ], 70 | "quotemark": [ 71 | true, 72 | "single", 73 | "avoid-escape" 74 | ], 75 | "radix": true, 76 | "semicolon": [ 77 | true, 78 | "never" 79 | ], 80 | "switch-default": true, 81 | "trailing-comma": [ 82 | false 83 | ], 84 | "triple-equals": [ 85 | true, 86 | "allow-null-check" 87 | ], 88 | "typedef": [ 89 | false, 90 | "call-signature", 91 | "parameter", 92 | "arrow-parameter", 93 | "property-declaration", 94 | "variable-declaration", 95 | "member-variable-declaration" 96 | ], 97 | "typedef-whitespace": [ 98 | true, 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | }, 106 | { 107 | "call-signature": "space", 108 | "index-signature": "space", 109 | "parameter": "space", 110 | "property-declaration": "space", 111 | "variable-declaration": "space" 112 | } 113 | ], 114 | "variable-name": [ 115 | true, 116 | "check-format", 117 | "allow-leading-underscore", 118 | "ban-keywords", 119 | "allow-pascal-case" 120 | ], 121 | "whitespace": [ 122 | true, 123 | "check-branch", 124 | "check-decl", 125 | "check-operator", 126 | "check-separator", 127 | "check-type" 128 | ] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supergraph", 3 | "description": "GraphQL Server Framework", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "prepublish": "npm run build", 7 | "build": "rm -rf dist && tsc", 8 | "lint": "tslint ./src/**/*.ts", 9 | "prettier": "prettier --single-quote --semi false --print-width 90 --write ./src/**/*.ts", 10 | "pretest": "npm run build", 11 | "test": "nyc mocha ./dist/**/*.spec.js" 12 | }, 13 | "main": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "license": "MIT", 16 | "author": "Kim Brandwijk ", 17 | "homepage": "https://github.com/supergraphql/supergraph#readme", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/supergraphql/supergraph.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/supergraphql/supergraph/issues" 24 | }, 25 | "keywords": [ 26 | "graphql", 27 | "api-gateway", 28 | "express-middleware", 29 | "koa" 30 | ], 31 | "files": [ 32 | "dist", 33 | "package.json", 34 | "README.md" 35 | ], 36 | "peerDependencies": { 37 | "apollo-link": "^1.0.3", 38 | "graphql": "^0.11.7" 39 | }, 40 | "dependencies": { 41 | "apollo-link-http": "^1.2.0", 42 | "apollo-server-express": "^1.2.0", 43 | "express": "^4.16.2", 44 | "graphql-add-middleware": "^0.1.3", 45 | "graphql-tools": "^2.8.0", 46 | "https-proxy-agent": "^2.1.0", 47 | "lodash": "^4.17.4", 48 | "memory-cache": "^0.2.0", 49 | "node-fetch": "^1.7.3" 50 | }, 51 | "devDependencies": { 52 | "@types/chai": "^4.0.5", 53 | "@types/express": "^4.0.39", 54 | "@types/lodash": "^4.14.85", 55 | "@types/memory-cache": "^0.2.0", 56 | "@types/mocha": "^2.2.44", 57 | "@types/node": "^8.0.52", 58 | "@types/node-fetch": "^1.6.7", 59 | "@types/sinon": "^4.0.0", 60 | "@types/zen-observable": "^0.5.3", 61 | "apollo-link": "^1.0.3", 62 | "chai": "^4.1.2", 63 | "codecov": "^3.0.0", 64 | "graphql": "^0.11.7", 65 | "mocha": "^4.0.1", 66 | "mocha-junit-reporter": "^1.15.0", 67 | "nyc": "^11.3.0", 68 | "prettier": "1.8.2", 69 | "sinon": "^4.1.2", 70 | "tslint": "^5.8.0", 71 | "typescript": "^2.6.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/SuperGraph.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { GraphQLSchema, GraphQLResolveInfo } from 'graphql' 3 | import { SuperGraphRouterResolver, SuperGraphRouterEvent } from './types' 4 | 5 | export class SuperGraph extends EventEmitter { 6 | public schemas: { mergedSchema?: GraphQLSchema, finalSchema?: GraphQLSchema, [name: string]: GraphQLSchema | string } = {} 7 | 8 | public resolvers: Array<{ 9 | path: string 10 | resolver: SuperGraphRouterResolver | ((event: SuperGraphRouterEvent) => Promise | any) 11 | }> = [] 12 | 13 | public middlewares: Array<{ 14 | path: string 15 | fn: ( 16 | parent: any, 17 | args: { [key: string]: any }, 18 | context: { [key: string]: any }, 19 | info: GraphQLResolveInfo, 20 | next: any 21 | ) => any 22 | }> = [] 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql' 2 | import { 3 | GraphQLResolveInfo, 4 | FieldNode 5 | } from 'graphql' 6 | import { SuperGraphRouterEvent } from './types' 7 | 8 | export const delegate = (event: SuperGraphRouterEvent) => ( 9 | operationType: 'query' | 'mutation' | 'subscription', 10 | operationName: string, 11 | args?: any 12 | ) => { 13 | return event.mergeInfo.delegate( 14 | operationType, 15 | operationName, 16 | args || event.args, 17 | event.context, 18 | event.info 19 | ) 20 | } 21 | 22 | export const delegateQuery = (event: SuperGraphRouterEvent) => ( 23 | query: string, 24 | vars?: { [key: string]: any } 25 | ) => { 26 | 27 | return graphql(event.context.supergraph.schemas.mergedSchema, query, null, null, vars).then(result => { 28 | return result.data 29 | }) 30 | } 31 | 32 | export const addTypenameField = (info: GraphQLResolveInfo): GraphQLResolveInfo => { 33 | const field: FieldNode = { 34 | kind: 'Field', 35 | name: { kind: 'Name', value: '__typename' } 36 | } 37 | 38 | return addFields(info, [field]) 39 | } 40 | 41 | export const addFields = ( 42 | info: GraphQLResolveInfo, 43 | fields: [FieldNode | string] | FieldNode | string 44 | ) => { 45 | const newInfo: GraphQLResolveInfo = JSON.parse(JSON.stringify(info)) 46 | 47 | if (!(fields instanceof Array)) { 48 | fields = [fields] 49 | } 50 | 51 | for (const field of fields) { 52 | if (typeof field === 'string') { 53 | newInfo.fieldNodes[0].selectionSet!.selections.push({ 54 | kind: 'Field', 55 | name: { kind: 'Name', value: field } 56 | }) 57 | } else { 58 | newInfo.fieldNodes[0].selectionSet!.selections.push(field) 59 | } 60 | } 61 | 62 | return newInfo 63 | } 64 | 65 | export const addHelpers = (event: SuperGraphRouterEvent) => { 66 | event.delegate = delegate(event) 67 | event.delegateQuery = delegateQuery(event) 68 | event.addFields = (fields: [FieldNode | string] | FieldNode | string) => 69 | (event.info = addFields(event.info, fields)) 70 | event.addTypenameField = () => (event.info = addTypenameField(event.info)) 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './middlewares' 2 | -------------------------------------------------------------------------------- /src/middlewares/base.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as sinon from 'sinon' 3 | import { base } from './base' 4 | import { SuperGraph } from '../SuperGraph' 5 | 6 | describe('base', () => { 7 | let mw 8 | let request: any 9 | 10 | beforeEach(function() { 11 | request = { supergraph: undefined } 12 | }) 13 | 14 | describe('Functionality', () => { 15 | it('should create context.supergraph if not part of the context', () => { 16 | mw = base(sinon.stub()) 17 | // tslint:disable-next-line:no-empty 18 | mw(request, null, () => {}) 19 | expect(request.supergraph).to.not.equal(undefined, 'supergraph object not added to the context') 20 | expect(request.supergraph.schemas).to.not.equal(undefined, 'schemas object not added to the supergraph context') 21 | expect(request.supergraph.resolvers).to.not.equal(undefined, 'resolvers object not added to the supergraph context') 22 | expect(request.supergraph.middlewares).to.not.equal(undefined, 'middlewares object not added to the supergraph context') 23 | }) 24 | 25 | it('should use existing context.supergraph', () => { 26 | mw = base(sinon.stub()) 27 | const existingRequest: any = { supergraph: new SuperGraph()} 28 | existingRequest.supergraph.schemas.test = '' 29 | // tslint:disable-next-line:no-empty 30 | mw(existingRequest, null, () => {}) 31 | expect(Object.keys(existingRequest.supergraph.schemas).length).to.equal(1, 'Existing context is overwritten') 32 | }) 33 | 34 | it('should pass any error to next', () => { 35 | const error = new Error() 36 | mw = base(sinon.stub().throws(error)) 37 | const next = sinon.spy() 38 | mw(request, null, next) 39 | expect(next.calledWith(error), 'Error not propagated to next()') 40 | }) 41 | }) 42 | 43 | describe('Express middleware function', () => { 44 | it('should return a function', () => { 45 | mw = base(sinon.stub()) 46 | expect(mw).to.be.a('Function', 'SuperGraph middleware should return a function') 47 | }) 48 | 49 | it('should accept three arguments', () => { 50 | mw = base(sinon.stub()) 51 | expect(mw.length).to.equal(3, 'SuperGraph middleware should return a function with 3 arguments') 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/middlewares/base.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, AsyncRequestHandler } from 'express' 2 | import { SuperGraph } from '../SuperGraph' 3 | 4 | 5 | 6 | export const base = ( 7 | fn: (req: Request, res: Response, next: NextFunction) => Promise | any 8 | ): AsyncRequestHandler => { 9 | return async (req: Request, res: Response, next: NextFunction): Promise => { 10 | if (!req.supergraph) { 11 | req.supergraph = new SuperGraph() 12 | } 13 | 14 | try { 15 | await fn(req, res, next) 16 | } catch (error) { 17 | next(error) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/middlewares/generateSchema.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, AsyncRequestHandler } from 'express' 2 | import { base } from './base' 3 | import { SuperGraphRouterEvent, SuperGraphRouterResolver } from '../types' 4 | import { GraphQLResolveInfo } from 'graphql/type/definition' 5 | import { addHelpers } from '../helpers' 6 | import { set, merge } from 'lodash' 7 | import { mergeSchemas } from 'graphql-tools' 8 | import { addMiddleware } from 'graphql-add-middleware' 9 | import { put, get } from 'memory-cache' 10 | 11 | export function generateSchema(): AsyncRequestHandler { 12 | return base(async (req: Request, res: Response, next: NextFunction): Promise => { 13 | generateSchemaImpl(req) 14 | next() 15 | }) 16 | } 17 | 18 | export function generateSchemaImpl(req: Request) { 19 | if (Object.keys(req.supergraph.schemas).length === 0) { 20 | throw new Error('No schemas defined') 21 | } 22 | 23 | // Only construct schema once 24 | if (!get('supergraph.mergedSchema')) { 25 | // Apply router resolvers 26 | const resolvers = (mergeInfo: any) => { 27 | const resolverBlocks: Array = req.supergraph.resolvers.map(resolver => 28 | generateResolverBlock(mergeInfo, resolver) 29 | ) 30 | 31 | const resolverObject = {} 32 | merge(resolverObject, ...resolverBlocks) 33 | return resolverObject 34 | } 35 | 36 | // MergeSchemas 37 | const schemasToMerge = Object.keys(req.supergraph.schemas).map( 38 | key => req.supergraph.schemas[key] 39 | ) 40 | 41 | req.supergraph.schemas.mergedSchema = mergeSchemas({ 42 | schemas: schemasToMerge, 43 | resolvers 44 | }) 45 | 46 | // Apply router middlewares 47 | for (const middleware of req.supergraph.middlewares) { 48 | addMiddleware(req.supergraph.schemas.mergedSchema, middleware.path, middleware.fn) 49 | } 50 | 51 | put('supergraph.mergedSchema', req.supergraph.schemas.mergedSchema) 52 | } else { 53 | req.supergraph.schemas.mergedSchema = get('supergraph.mergedSchema') 54 | } 55 | 56 | req.supergraph.emit('schemaGenerated', req.supergraph.schemas.mergedSchema) 57 | } 58 | 59 | function generateResolverBlock( 60 | mergeInfo: any, 61 | resolver: { 62 | path: string 63 | resolver: SuperGraphRouterResolver | ((event: SuperGraphRouterEvent) => Promise | any) 64 | } 65 | ): Promise { 66 | // Create object from path -> apparently, can also use _.set and _.get here :D 67 | let resolverObject: any = {} 68 | 69 | let resolveFn: any 70 | 71 | if (typeof resolver.resolver === 'function') { 72 | // This is a 'normal' resolver function 73 | resolveFn = wrap(resolver.resolver, mergeInfo) 74 | } else { 75 | resolveFn = { 76 | ...resolver.resolver, 77 | resolve: wrap(resolver.resolver.resolve, mergeInfo) 78 | } 79 | } 80 | 81 | set(resolverObject, resolver.path, resolveFn) 82 | 83 | return resolverObject 84 | } 85 | 86 | function wrap(fn: ((event: SuperGraphRouterEvent) => Promise | any), mergeInfo: any) { 87 | return async ( 88 | parent: any, 89 | args: { [key: string]: any }, 90 | context: { [key: string]: any }, 91 | info: GraphQLResolveInfo 92 | ) => { 93 | const event: any = { parent, args, context, info, mergeInfo: mergeInfo } 94 | addHelpers(event) 95 | return await fn(event) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { base } from './base' 2 | export { schema } from './schema' 3 | export { remoteSchema } from './remoteSchema' 4 | export { resolver, resolvers } from './resolver' 5 | export { serve } from './serve' 6 | export { use } from './use' 7 | export { generateSchema } from './generateSchema' 8 | export { transform } from './transform' 9 | -------------------------------------------------------------------------------- /src/middlewares/remoteSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | import { ApolloLink } from 'apollo-link' 3 | import fetch from 'node-fetch' 4 | import { createHttpLink } from 'apollo-link-http' 5 | import { introspectSchema, makeRemoteExecutableSchema } from 'graphql-tools' 6 | import { Request, Response, AsyncRequestHandler, NextFunction } from 'express' 7 | import { base } from './base' 8 | import { merge } from 'lodash' 9 | import { isBoolean } from 'util' 10 | import * as HttpsProxyAgent from 'https-proxy-agent' 11 | import { get, put } from 'memory-cache' 12 | 13 | export function remoteSchema({ 14 | name, 15 | uri, 16 | introspectionSchema, 17 | link, 18 | authenticationToken, 19 | forwardHeaders = false 20 | }: { 21 | name?: string 22 | uri?: string 23 | introspectionSchema?: GraphQLSchema 24 | link?: ApolloLink 25 | authenticationToken?: (req: Request) => string 26 | forwardHeaders?: boolean | Array 27 | }): AsyncRequestHandler { 28 | if (!uri && !link) { 29 | throw new Error('Specify either uri or link to define remote schema') 30 | } 31 | 32 | return base(async (req: Request, res: Response, next: NextFunction): Promise => { 33 | if (link === undefined) { 34 | const httpLink: ApolloLink = createHttpLink({ 35 | uri, 36 | fetch: fetch as any, 37 | fetchOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {} 38 | }) 39 | 40 | if (authenticationToken !== undefined) { 41 | link = new ApolloLink((operation, forward) => { 42 | operation.setContext((ctx: Request) => { 43 | let headers: any = {} 44 | if (ctx && forwardHeaders) { 45 | if (isBoolean(forwardHeaders)) { 46 | headers = ctx.headers 47 | } else { 48 | forwardHeaders.forEach(h => (headers[h] = ctx.headers[h])) 49 | } 50 | } 51 | if (ctx && authenticationToken) { 52 | merge(headers, { Authorization: `Bearer ${authenticationToken(ctx)}` }) 53 | } 54 | 55 | return { headers } 56 | }) 57 | return forward!(operation) 58 | }).concat(httpLink) 59 | } else { 60 | link = httpLink 61 | } 62 | } 63 | 64 | if (name === undefined) { 65 | name = `schema${Object.keys(req.supergraph.schemas).length}` 66 | } 67 | 68 | if (introspectionSchema === undefined) { 69 | if (!get(`supergraph.${name}.introspection`)) { 70 | introspectionSchema = await introspectSchema(link) 71 | put(`supergraph.${name}.introspection`, introspectionSchema) 72 | } else { 73 | introspectionSchema = get(`supergraph.${name}.introspection`) 74 | } 75 | } 76 | 77 | const executableSchema = makeRemoteExecutableSchema({ 78 | schema: introspectionSchema, 79 | link 80 | }) 81 | 82 | req.supergraph.schemas[name] = executableSchema 83 | 84 | next() 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /src/middlewares/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, AsyncRequestHandler } from 'express' 2 | import { base } from './base' 3 | import { SuperGraphRouterResolver, SuperGraphRouterEvent } from '../types' 4 | 5 | // Todo: also support function for path 6 | export function resolver( 7 | path: string, 8 | resolve: SuperGraphRouterResolver | ((event: SuperGraphRouterEvent) => Promise | any) 9 | ): AsyncRequestHandler { 10 | return base(async (req: Request, res: Response, next: NextFunction): Promise => { 11 | // Guard 12 | if (req.supergraph.resolvers.some(r => r.path === path)) { 13 | throw new Error(`${path}: You can only specify one resolver for a path`) 14 | } 15 | 16 | req.supergraph.resolvers.push({ path, resolver: resolve }) 17 | 18 | next() 19 | }) 20 | } 21 | 22 | export function resolvers(def: { 23 | [key: string]: { 24 | [key: string]: SuperGraphRouterResolver | ((event: SuperGraphRouterEvent) => Promise | any) 25 | } 26 | }): AsyncRequestHandler { 27 | return base(async (req: Request, res: Response, next: NextFunction): Promise => { 28 | // Guard 29 | for (const parent of Object.keys(def)) { 30 | for (const field of Object.keys(def[parent])) { 31 | const path = `${parent}.${field}` 32 | if (req.supergraph.resolvers.some(r => r.path === path)) { 33 | throw new Error(`${path}: You can only specify one resolver for a path`) 34 | } 35 | 36 | req.supergraph.resolvers.push({ path, resolver: def[parent][field] }) 37 | } 38 | } 39 | 40 | next() 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/middlewares/schema.spec.ts: -------------------------------------------------------------------------------- 1 | import { schema } from './schema' 2 | import { expect } from 'chai' 3 | import * as sinon from 'sinon' 4 | import { makeExecutableSchema } from 'graphql-tools' 5 | 6 | describe('schema', () => { 7 | let mw 8 | let request: any 9 | 10 | beforeEach(function() { 11 | request = { supergraph: undefined } 12 | }) 13 | 14 | describe('Express middleware function', () => { 15 | it('should return a function', () => { 16 | mw = schema({schema: ''}) 17 | expect(mw).to.be.a('Function', 'SuperGraph middleware should return a function') 18 | }) 19 | 20 | it('should accept three arguments', () => { 21 | mw = schema({schema: ''}) 22 | expect(mw.length).to.equal(3, 'SuperGraph middleware should return a function with 3 arguments') 23 | }) 24 | 25 | it('should call next() once', function() { 26 | mw = schema({schema: ''}) 27 | const nextSpy = sinon.spy() 28 | 29 | mw(request, null, nextSpy) 30 | expect(nextSpy.calledOnce).to.equal(true, 'next() is not called') 31 | }) 32 | }) 33 | 34 | describe('Functionality', () => { 35 | it('should use the passed in name as key in the context', () => { 36 | mw = schema({name: 'Test', schema: 'type Query { dummy: String }'}) 37 | // tslint:disable-next-line:no-empty 38 | mw(request, null, () => {}) 39 | expect(Object.keys(request.supergraph.schemas)).to.contain('Test', 'Schema not added with passed in name') 40 | }) 41 | 42 | 43 | it('should add a schema passed in as string to the context', () => { 44 | mw = schema('type Query { dummy: String }') 45 | // tslint:disable-next-line:no-empty 46 | mw(request, null, () => {}) 47 | expect(Object.keys(request.supergraph.schemas).length).to.equal(1, 'Schema not added to context') 48 | }) 49 | 50 | it('should add a schema passed in as an object with string to the context', () => { 51 | mw = schema({schema: 'type Query { dummy: String }'}) 52 | // tslint:disable-next-line:no-empty 53 | mw(request, null, () => {}) 54 | expect(Object.keys(request.supergraph.schemas).length).to.equal(1, 'Schema not added to context') 55 | }) 56 | 57 | it('should add a schema passed in as GraphQLSchema to the context', () => { 58 | mw = schema(makeExecutableSchema({ typeDefs: 'type Query { dummy: String }'})) 59 | // tslint:disable-next-line:no-empty 60 | mw(request, null, () => {}) 61 | expect(Object.keys(request.supergraph.schemas).length).to.equal(1, 'Schema not added to context') 62 | }) 63 | 64 | it('should add a schema passed in as an object with GraphQLSchema to the context', () => { 65 | mw = schema({schema: makeExecutableSchema({ typeDefs: 'type Query { dummy: String }'})}) 66 | // tslint:disable-next-line:no-empty 67 | mw(request, null, () => {}) 68 | expect(Object.keys(request.supergraph.schemas).length).to.equal(1, 'Schema not added to context') 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/middlewares/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | import { Request, Response, AsyncRequestHandler, NextFunction } from 'express' 3 | import { base } from './base' 4 | import { makeExecutableSchema } from 'graphql-tools' 5 | 6 | export function schema( 7 | def: { name?: string; schema: GraphQLSchema | string } | GraphQLSchema | string 8 | ): AsyncRequestHandler { 9 | return base((req: Request, res: Response, next: NextFunction): any => { 10 | if (typeof def === 'string' || def instanceof GraphQLSchema) { 11 | def = { schema: def } 12 | } 13 | 14 | if (!def.name) { 15 | def.name = `schema${Object.keys(req.supergraph.schemas).length}` 16 | } 17 | 18 | if (typeof def.schema === 'string') { 19 | try { 20 | const actualSchema = makeExecutableSchema({typeDefs: def.schema}) 21 | def.schema = actualSchema 22 | } catch (e) {} 23 | } 24 | 25 | req.supergraph.schemas[def.name] = def.schema 26 | 27 | next() 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/middlewares/serve.ts: -------------------------------------------------------------------------------- 1 | import { ExpressHandler, graphqlExpress } from 'apollo-server-express' 2 | import { Request, Response, NextFunction } from 'express' 3 | import { generateSchemaImpl } from './generateSchema' 4 | import { SuperGraphServerOptions } from '../types' 5 | 6 | export function serve(serverOptions?: SuperGraphServerOptions): ExpressHandler { 7 | return async (req: Request, res: Response, next: NextFunction): Promise => { 8 | await generateSchemaImpl(req) 9 | return graphqlExpress({ 10 | ...serverOptions, 11 | schema: req.supergraph.schemas.finalSchema || req.supergraph.schemas.mergedSchema, 12 | context: req 13 | })(req, res, next) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/middlewares/transform.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, AsyncRequestHandler } from 'express' 2 | import { base } from './base' 3 | import { parse, TypeDefinitionNode, GraphQLSchema } from 'graphql' 4 | import { get, put } from 'memory-cache' 5 | import { mergeSchemas } from 'graphql-tools' 6 | 7 | export function transform(finalSchema: string): AsyncRequestHandler { 8 | return base((req: Request, res: Response, next: NextFunction): any => { 9 | req.supergraph.on('schemaGenerated', generatedSchema => { 10 | if (!get('supergraph.finalSchema')) { 11 | const doc = parse(finalSchema) 12 | 13 | // Merge, taking the definitions from the final schema. 14 | // Unfortunately, this breaks resolvers (https://github.com/apollographql/graphql-tools/issues/504) 15 | // And it doesn't apply onTypeConflict on the entire Query/Mutation types, just to individual fields 16 | const schemaDef: GraphQLSchema = mergeSchemas({ 17 | schemas: [req.supergraph.schemas.mergedSchema, finalSchema], 18 | onTypeConflict: (l, r) => r 19 | }) as GraphQLSchema 20 | 21 | // Restore the resolvers for the query type, or remove it or any of its fields if not part of the final schema 22 | const queryType = schemaDef.getQueryType() 23 | if (queryType) { 24 | if (!doc.definitions.map(d => (d as TypeDefinitionNode).name.value).includes(queryType.name)) { 25 | delete (schemaDef as any)._queryType 26 | } else { 27 | const queryFields = queryType.getFields() 28 | const queryFieldKeys = Object.keys(queryFields) 29 | const newQueryTypeFields = (doc.definitions.find( 30 | d => (d as TypeDefinitionNode).name.value === queryType.name 31 | ) as any).fields.map((f: any) => f.name.value) 32 | for (const key of queryFieldKeys) { 33 | if (newQueryTypeFields.includes(key)) { 34 | queryFields[key].resolve = req.supergraph.schemas.mergedSchema.getQueryType().getFields()[ 35 | key 36 | ].resolve 37 | } else { 38 | delete schemaDef.getQueryType().getFields()[key] 39 | } 40 | } 41 | } 42 | } 43 | 44 | // Restore the resolvers for the mutation type, or remove it or any of its fields if not part of the final schema 45 | const mutationType = schemaDef.getMutationType() 46 | if (mutationType) { 47 | if (!doc.definitions.map(d => (d as TypeDefinitionNode).name.value).includes(mutationType.name)) { 48 | delete (schemaDef as any)._mutationType 49 | } else { 50 | const mutationFields = mutationType.getFields() 51 | const mutationFieldKeys = Object.keys(mutationFields) 52 | const newMutationTypeFields = (doc.definitions.find( 53 | d => (d as TypeDefinitionNode).name.value === mutationType.name 54 | ) as any).fields.map((f: any) => f.name.value) 55 | for (const key of mutationFieldKeys) { 56 | if (newMutationTypeFields.includes(key)) { 57 | mutationFields[key].resolve = req.supergraph.schemas.mergedSchema.getMutationType().getFields()[ 58 | key 59 | ].resolve 60 | } else { 61 | delete schemaDef.getMutationType().getFields()[key] 62 | } 63 | } 64 | } 65 | } 66 | 67 | // Restore the resolvers for the subscription type, or remove it or any of its fields if not part of the final schema 68 | const subscriptionType = schemaDef.getSubscriptionType() 69 | if (subscriptionType) { 70 | if ( 71 | !doc.definitions.map(d => (d as TypeDefinitionNode).name.value).includes(subscriptionType.name) 72 | ) { 73 | delete (schemaDef as any)._subscriptionType 74 | } else { 75 | const subscriptionFields = subscriptionType.getFields() 76 | const subscriptionFieldKeys = Object.keys(subscriptionFields) 77 | const newSubscriptionTypeFields = (doc.definitions.find( 78 | d => (d as TypeDefinitionNode).name.value === subscriptionType.name 79 | ) as any).fields.map((f: any) => f.name.value) 80 | for (const key of subscriptionFieldKeys) { 81 | if (newSubscriptionTypeFields.includes(key)) { 82 | subscriptionFields[ 83 | key 84 | ].resolve = req.supergraph.schemas.mergedSchema.getSubscriptionType().getFields()[key].resolve 85 | } else { 86 | delete schemaDef.getSubscriptionType().getFields()[key] 87 | } 88 | } 89 | } 90 | } 91 | 92 | req.supergraph.schemas.finalSchema = schemaDef 93 | put(`supergraph.finalSchema`, schemaDef) 94 | } else { 95 | req.supergraph.schemas.finalSchema = get(`supergraph.finalSchema`) 96 | } 97 | }) 98 | 99 | next() 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /src/middlewares/use.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, AsyncRequestHandler, NextFunction } from 'express' 2 | import { SuperGraphRouterMiddlewareHandler } from '../types' 3 | import { base } from './base' 4 | import { GraphQLResolveInfo } from 'graphql' 5 | import { addHelpers } from '../helpers' 6 | 7 | export function use(path: string, fn: SuperGraphRouterMiddlewareHandler): AsyncRequestHandler { 8 | return base(async (req: Request, res: Response, next: NextFunction): Promise => { 9 | // wrap the function 10 | const middlewareFunction = async ( 11 | parent: any, 12 | args: { [key: string]: any }, 13 | context: { [key: string]: any }, 14 | info: GraphQLResolveInfo, 15 | nxt: any 16 | ) => { 17 | const event: any = { parent, args, context, info } 18 | addHelpers(event) 19 | return await fn(event, nxt) 20 | } 21 | req.supergraph.middlewares.push({ path, fn: middlewareFunction }) 22 | 23 | next() 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLTypeResolver, 4 | GraphQLIsTypeOfFn, 5 | GraphQLResolveInfo, 6 | FieldNode, 7 | ValidationContext, 8 | GraphQLFieldResolver 9 | } from 'graphql' 10 | import { Request } from 'express' 11 | import { LogFunction } from 'apollo-server-core' 12 | 13 | export interface SuperGraphMiddlewareHandler { 14 | (context: Request): void 15 | } 16 | 17 | export interface SuperGraphContext { 18 | schemas: { [key: string]: GraphQLSchema | string } 19 | req?: Request 20 | [key: string]: any 21 | } 22 | 23 | export interface SuperGraphRouterMiddlewareHandler { 24 | (event: SuperGraphRouterEvent, next: any): Promise | any 25 | } 26 | 27 | export interface SuperGraphRouterResolver { 28 | fragment?: string 29 | resolve: (event: SuperGraphRouterEvent) => Promise | any 30 | __resolveType?: GraphQLTypeResolver 31 | __isTypeOf?: GraphQLIsTypeOfFn 32 | } 33 | 34 | export interface SuperGraphRouterEvent { 35 | parent: any 36 | args: { [key: string]: any } 37 | context: { [key: string]: any } 38 | info: GraphQLResolveInfo 39 | delegate: ( 40 | operationType: 'query' | 'mutation' | 'subscription', 41 | operationName: string, 42 | args?: any 43 | ) => any 44 | delegateQuery: (query: string, vars?: { [key: string]: any }) => any 45 | addTypeNameField: () => void 46 | addFields: (fields: [FieldNode | string] | FieldNode | string) => void 47 | 48 | [key: string]: any 49 | } 50 | 51 | export interface SuperGraphServerOptions { 52 | formatError?: Function 53 | rootValue?: any 54 | logFunction?: LogFunction 55 | formatParams?: Function 56 | validationRules?: Array<(context: ValidationContext) => any> 57 | formatResponse?: Function 58 | fieldResolver?: GraphQLFieldResolver 59 | debug?: boolean 60 | tracing?: boolean 61 | cacheControl?: boolean 62 | } 63 | -------------------------------------------------------------------------------- /src/typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { SuperGraph } from '../SuperGraph'; 3 | 4 | declare module 'express' { 5 | export interface Request { 6 | supergraph: SuperGraph 7 | } 8 | 9 | export interface AsyncRequestHandler { 10 | (req: Request, res: Response, next: NextFunction): Promise | any 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/typings/graphql-add-middleware.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphql-add-middleware' 2 | -------------------------------------------------------------------------------- /src/typings/https-proxy-agent.ts: -------------------------------------------------------------------------------- 1 | declare module 'https-proxy-agent' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "lib": ["es7", "dom", "esnext.asynciterable"], 5 | "module": "commonjs", 6 | "target": "es5", 7 | "noImplicitAny": true, 8 | "suppressImplicitAnyIndexErrors": true, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "noUnusedLocals": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "rootDir": "./src", 15 | "outDir": "./dist", 16 | "removeComments": false 17 | }, 18 | "exclude": ["node_modules", "dist", "examples"] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-construct": true, 47 | "no-parameter-properties": true, 48 | "no-debugger": true, 49 | "no-duplicate-variable": true, 50 | "no-empty": [true, "allow-empty-catch"], 51 | "no-eval": true, 52 | "no-inferrable-types": false, 53 | "no-internal-module": true, 54 | "no-null-keyword": false, 55 | "no-require-imports": false, 56 | "no-shadowed-variable": true, 57 | "no-switch-case-fall-through": true, 58 | "no-trailing-whitespace": true, 59 | "no-unused-expression": true, 60 | "no-use-before-declare": false, 61 | "no-var-keyword": true, 62 | "object-literal-sort-keys": false, 63 | "one-line": [ 64 | true, 65 | "check-open-brace", 66 | "check-catch", 67 | "check-else", 68 | "check-finally", 69 | "check-whitespace" 70 | ], 71 | "quotemark": [ 72 | true, 73 | "single", 74 | "avoid-escape" 75 | ], 76 | "radix": true, 77 | "semicolon": [ 78 | true, 79 | "never" 80 | ], 81 | "switch-default": true, 82 | "trailing-comma": [ 83 | false 84 | ], 85 | "triple-equals": [ 86 | true, 87 | "allow-null-check" 88 | ], 89 | "typedef": [ 90 | false, 91 | "call-signature", 92 | "parameter", 93 | "arrow-parameter", 94 | "property-declaration", 95 | "variable-declaration", 96 | "member-variable-declaration" 97 | ], 98 | "typedef-whitespace": [ 99 | true, 100 | { 101 | "call-signature": "nospace", 102 | "index-signature": "nospace", 103 | "parameter": "nospace", 104 | "property-declaration": "nospace", 105 | "variable-declaration": "nospace" 106 | }, 107 | { 108 | "call-signature": "space", 109 | "index-signature": "space", 110 | "parameter": "space", 111 | "property-declaration": "space", 112 | "variable-declaration": "space" 113 | } 114 | ], 115 | "variable-name": [ 116 | true, 117 | "check-format", 118 | "allow-leading-underscore", 119 | "ban-keywords", 120 | "allow-pascal-case" 121 | ], 122 | "whitespace": [ 123 | true, 124 | "check-branch", 125 | "check-decl", 126 | "check-operator", 127 | "check-separator", 128 | "check-type" 129 | ] 130 | } 131 | } 132 | --------------------------------------------------------------------------------