├── .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 | []()
4 | []()
5 | [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------