Resolve comflicts

This commit is contained in:
unicodeveloper 2018-06-25 07:48:14 +01:00
commit ef5e1e1758
81 changed files with 2781 additions and 1032 deletions

View file

@ -6,6 +6,7 @@ All of the packages in the `apollo-server` repo are released with the same versi
* Upgrade `subscription-transport-ws` to 0.9.9 for Graphiql
* Remove tests and guaranteed support for Node 4 [PR #1024](https://github.com/apollographql/apollo-server/pull/1024)
* Cleanup docs [PR #1233](https://github.com/apollographql/apollo-server/pull/1233/files)
### v1.3.6

336
README.md
View file

@ -4,301 +4,217 @@
[![Build Status](https://circleci.com/gh/apollographql/apollo-cache-control-js.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-cache-control-js)
[![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://www.apollographql.com/#slack)
FIXME update README for 2.x; refer to 1.x branch for currently unsupported web frameworks and encourage community to help port ApolloServer to other frameworks.
Apollo Server is a community-maintained open-source GraphQL server. It works with pretty much all Node.js HTTP server frameworks, and we're happy to take PRs for more! Apollo Server works with any GraphQL schema built with [GraphQL.js](https://github.com/graphql/graphql-js), so you can build your schema with that directly or with a convenience library such as [graphql-tools](https://www.apollographql.com/docs/graphql-tools/).
## Documentation
[Read the docs!](https://www.apollographql.com/docs/apollo-server/)
[Read the docs!](https://www.apollographql.com/docs/apollo-server/v2)
## Principles
Apollo Server is built with the following principles in mind:
* **By the community, for the community**: Apollo Server's development is driven by the needs of developers
* **Simplicity**: by keeping things simple, Apollo Server is easier to use, easier to contribute to, and more secure
* **Performance**: Apollo Server is well-tested and production-ready - no modifications needed
- **By the community, for the community**: Apollo Server's development is driven by the needs of developers
- **Simplicity**: by keeping things simple, Apollo Server is easier to use, easier to contribute to, and more secure
- **Performance**: Apollo Server is well-tested and production-ready - no modifications needed
Anyone is welcome to contribute to Apollo Server, just read [CONTRIBUTING.md](./CONTRIBUTING.md), take a look at the [roadmap](./ROADMAP.md) and make your first PR!
## Getting started
Apollo Server is super easy to set up. Just `npm install apollo-server-<variant>`, write a GraphQL schema, and then use one of the following snippets to get started. For more info, read the [Apollo Server docs](https://www.apollographql.com/docs/apollo-server/). To experiment a live example of Apollo Server, create an [Apollo Launchpad](https://launchpad.graphql.com). Downloading the pad will provide you a local Apollo Server project.
Apollo Server is super easy to set up. Just `npm install apollo-server-<integration>`, write a GraphQL schema, and then use one of the following snippets to get started. For more info, read the [Apollo Server docs](https://www.apollographql.com/docs/apollo-server/v2).
### Installation
Just run `npm install --save apollo-server-<variant>` and you're good to go!
Run `npm install --save apollo-server-<integration>` and you're good to go!
where `<variant>` is one of the following:
```js
const { ApolloServer, gql } = require('apollo-server');
* `express`
* `koa`
* `hapi`
* `restify`
* `lambda`
* `micro`
* `azure-functions`
* `adonis`
// The GraphQL schema
const typeDefs = gql`
type Query {
"A simple type for getting started!"
hello: String
}
`;
// A map of functions which return data for the schema.
const resolvers = {
Query: {
hello: () => 'world'
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
```
## Integrations
Often times, Apollo Server needs to be run with a particular integration. To start, run `npm install --save apollo-server-<integration>` where `<integration>` is one of the following:
- `express`
- `hapi`
- `lambda`
- `cloudflare`
If a framework is not on this list and it should be supported, please open a PR.
### Express
```js
import express from 'express';
import bodyParser from 'body-parser';
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');
const myGraphQLSchema = // ... define or import your schema here!
const PORT = 3000;
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const app = express();
server.applyMiddleware({ app });
// bodyParser is needed just for POST.
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema: myGraphQLSchema }));
app.get('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); // if you want GraphiQL enabled
app.listen(PORT);
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
```
### Connect
```js
import connect from 'connect';
import bodyParser from 'body-parser';
import query from 'connect-query';
import { graphqlConnect } from 'apollo-server-express';
import http from 'http';
const connect = require('connect');
const { ApolloServer, gql } = require('apollo-server-express');
const query = require('qs-middleware');
const PORT = 3000;
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const app = connect();
const path = '/graphql';
// bodyParser is only needed for POST.
app.use('/graphql', bodyParser.json());
// query is only needed for GET.
app.use('/graphql', query());
app.use('/graphql', graphqlConnect({ schema: myGraphQLSchema }));
server.use(query());
server.applyMiddleware({ app, path });
http.createServer(app).listen(PORT);
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
```
### Hapi
> Note; `qs-middleware` is only required if running outside of Meteor
Now with the Hapi plugins `graphqlHapi` and `graphiqlHapi` you can pass a route object that includes options to be applied to the route. The example below enables CORS on the `/graphql` route.
### Hapi
The code below requires Hapi 17 or higher.
```js
import Hapi from 'hapi';
import { graphqlHapi } from 'apollo-server-hapi';
const HOST = 'localhost';
const PORT = 3000;
const { ApolloServer, gql } = require('apollo-server-hapi');
const Hapi = require('hapi');
async function StartServer() {
const server = new Hapi.server({
host: HOST,
port: PORT,
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Hapi.server({
port: 4000
});
await server.register({
plugin: graphqlHapi,
options: {
path: '/graphql',
graphqlOptions: {
schema: myGraphQLSchema,
},
route: {
cors: true,
},
},
await server.applyMiddleware({
app,
});
try {
await server.start();
} catch (err) {
console.log(`Error while starting server: ${err.message}`);
}
await server.installSubscriptionHandlers(app.listener);
console.log(`Server running at: ${server.info.uri}`);
await app.start();
}
StartServer();
StartServer().catch(error => console.log(error));
```
### Koa
### Context
The context is created for each request. The following code snippet shows the creation of a context. The arguments are the `request`, the request, and `h`, the response toolkit.
```js
import koa from 'koa'; // koa@2
import koaRouter from 'koa-router'; // koa-router@next
import koaBody from 'koa-bodyparser'; // koa-bodyparser@next
import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa';
const app = new koa();
const router = new koaRouter();
const PORT = 3000;
// koaBody is needed just for POST.
router.post('/graphql', koaBody(), graphqlKoa({ schema: myGraphQLSchema }));
router.get('/graphql', graphqlKoa({ schema: myGraphQLSchema }));
router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' }));
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(PORT);
```
### Restify
```js
import restify from 'restify';
import { graphqlRestify, graphiqlRestify } from 'apollo-server-restify';
const PORT = 3000;
const server = restify.createServer({
title: 'Apollo Server',
});
const graphQLOptions = { schema: myGraphQLSchema };
server.use(restify.plugins.bodyParser());
server.use(restify.plugins.queryParser());
server.post('/graphql', graphqlRestify(graphQLOptions));
server.get('/graphql', graphqlRestify(graphQLOptions));
server.get('/graphiql', graphiqlRestify({ endpointURL: '/graphql' }));
server.listen(PORT, () => console.log(`Listening on ${PORT}`));
new ApolloServer({
typeDefs,
resolvers,
context: async ({ request, h }) => {
return { ... };
},
})
```
### AWS Lambda
Lambda function should be run with [Node.js 4.3 or v6.1](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-using-old-runtime.html#nodejs-prog-model-runtime-support-policy). Requires an API Gateway with Lambda Proxy Integration.
Apollo Server can be run on Lambda and deployed with AWS Serverless Application Model (SAM). It requires an API Gateway with Lambda Proxy Integration.
```js
var server = require('apollo-server-lambda');
const { ApolloServer, gql } = require('apollo-server-lambda');
exports.handler = server.graphqlLambda({ schema: myGraphQLSchema });
```
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
### ZEIT Micro
Requires the [Micro](https://github.com/zeit/micro) module
```js
const server = require('apollo-server-micro');
module.exports = server.microGraphql({ schema: myGraphQLSchema });
```
### Adonis Framework
```js
// start/routes.js
const { graphqlAdonis } = require('apollo-server-adonis');
const Route = use('Route');
Route.post('/graphql', graphqlAdonis({ schema: myGraphQLSchema }));
Route.get('/graphql', graphqlAdonis({ schema: myGraphQLSchema }));
```
## Options
Apollo Server can be configured with an options object with the following fields:
* **schema**: the GraphQLSchema to be used
* **context**: the context value passed to resolvers during GraphQL execution
* **rootValue**: the value passed to the first resolve function
* **formatError**: a function to apply to every error before sending the response to clients
* **validationRules**: additional GraphQL validation rules to be applied to client-specified queries
* **formatParams**: a function applied for each query in a batch to format parameters before execution
* **formatResponse**: a function applied to each response after execution
* **tracing**: when set to true, collect and expose trace data in the [Apollo Tracing format](https://github.com/apollographql/apollo-tracing)
* **logFunction**: a function called for logging events such as execution times
* **fieldResolver**: a custom default field resolver
* **debug**: a boolean that will print additional debug logging if execution errors occur
* **cacheControl**: when set to true, enable built-in support for Apollo Cache Control
All options except for `schema` are optional.
### Whitelisting
The `formatParams` function can be used in combination with the `OperationStore` to enable whitelisting.
```js
const store = new OperationStore(Schema);
store.put('query testquery{ testString }');
graphqlOptions = {
schema: Schema,
formatParams(params) {
params['parsedQuery'] = store.get(params.operationName);
delete params['queryString']; // Or throw if this is provided.
return params;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
```
## Comparison with `express-graphql`
const server = new ApolloServer({ typeDefs, resolvers });
Both Apollo Server and [`express-graphql`](https://github.com/graphql/express-graphql) are GraphQL servers for Node.js, built on top of the [`graphql-js` reference implementation](https://github.com/graphql/graphql-js), but there are a few key differences:
* `express-graphql` works with Express and Connect, Apollo Server supports Express, Connect, Hapi, Koa and Restify.
* Compared to `express-graphql`, Apollo Server has a simpler interface and supports exactly one way of passing queries.
* Apollo Server separates serving [GraphiQL](https://github.com/graphql/graphiql) (an in-browser IDE for exploring GraphQL) from responding to GraphQL requests.
* `express-graphql` contains code for parsing HTTP request bodies, Apollo Server leaves that to standard packages like body-parser.
* Apollo Server includes an `OperationStore` to easily manage whitelisting.
* Apollo Server is built with TypeScript.
### application/graphql requests
`express-graphql` supports the `application/graphql` Content-Type for requests, which is an alternative to `application/json` request with only the query part sent as text. In the same way that we use `bodyParser.json` to parse `application/json` requests for apollo-server, we can use `bodyParser.text` plus one extra step in order to also parse `application/graphql` requests. Here's an example for Express:
```js
import express from 'express';
import bodyParser from 'body-parser';
import { graphqlExpress } from 'apollo-server-express';
const myGraphQLSchema = // ... define or import your schema here!
const helperMiddleware = [
bodyParser.json(),
bodyParser.text({ type: 'application/graphql' }),
(req, res, next) => {
if (req.is('application/graphql')) {
req.body = { query: req.body };
}
next();
}
];
express()
.use('/graphql', ...helperMiddleware, graphqlExpress({ schema: myGraphQLSchema }))
  .listen(3000);
exports.graphqlHandler = server.createHandler();
```
## Apollo Server Development
If you want to develop Apollo Server locally you must follow the following instructions:
* Fork this repository
- Fork this repository
* Install the Apollo Server project in your computer
- Install the Apollo Server project in your computer
```
git clone https://github.com/[your-user]/apollo-server
cd apollo-server
npm install
cd packages/apollo-server-<variant>/
cd packages/apollo-server-<integration>/
npm link
```
* Install your local Apollo Server in other App
- Install your local Apollo Server in other App
```
cd ~/myApp
npm link apollo-server-<variant>
npm link apollo-server-<integration>
```

View file

@ -20,9 +20,10 @@ sidebar_categories:
- features/errors
- features/apq
- features/data-sources
- features/cdn
- features/subscriptions
- features/logging
- features/file-uploads
- features/metrics
- features/scalars-enums
- features/unions-interfaces
- features/directives

View file

@ -14,7 +14,7 @@
"hexo-renderer-less": "0.2.0",
"hexo-renderer-marked": "0.3.2",
"hexo-server": "0.3.2",
"meteor-theme-hexo": "1.0.14"
"meteor-theme-hexo": "1.0.15"
},
"scripts": {
"start": "npm run build && chexo apollo-hexo-config -- server",

View file

@ -7,7 +7,3 @@ Intro about what to watch for?
## ENGINE
## formatError
## logFunction?
* logFunction should be documented https://github.com/apollographql/apollo-server/blob/master/packages/apollo-server-core/src/runQuery.ts#L64

View file

@ -1,3 +1,196 @@
---
title: Lambda
title: Deploying with AWS Lambda
sidebar_title: Lambda
description: How to deploy Apollo Server with AWS Lambda
---
AWS Lambda is a service that lets you run code without provisioning or managing servers. You pay only for the compute time you consume-there is no charge when your code is not running.
Learn how to integrate Apollo Server 2 with AWS Lambda. First, install the `apollo-server-lambda` package:
```sh
npm install apollo-server-lambda@rc
```
## Deploying with AWS Serverless Application Model (SAM)
To deploy the AWS Lambda function, you must create a Cloudformation Template and a S3 bucket to store the artifact (zip of source code) and template. You'll use the [AWS Command Line Interface](https://aws.amazon.com/cli/).
#### 1. Write the API handlers
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
exports.graphqlHandler = server.createHandler();
```
#### 2. Create an S3 bucket
The bucket name must be universally unique.
```bash
aws s3 mb s3://<bucket name>
```
#### 3. Create the Template
This will look for a file called `graphql.js` with the export `graphqlHandler`. It creates one API endpoints:
* `/graphql` (GET and POST)
In a file called `template.yaml`:
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
GraphQL:
Type: AWS::Serverless::Function
Properties:
Handler: graphql.graphqlHandler
Runtime: nodejs8.10
Events:
GetRequest:
Type: Api
Properties:
Path: /graphql
Method: get
PostRequest:
Type: Api
Properties:
Path: /graphql
Method: post
```
#### 4. Package source code and dependencies
Read and transform the template, created in the previous step. Package and upload the artifact to the S3 bucket and generate another template for the deployment.
```sh
aws cloudformation package \
--template-file template.yaml \
--output-template-file serverless-output.yaml \
--s3-bucket <bucket-name>
```
#### 5. Deploy the API
Create the Lambda Function and API Gateway for GraphQL. In the example below, `prod` stands for production. However, you can use any name to represent it.
```
aws cloudformation deploy \
--template-file serverless-output.yaml \
--stack-name prod \
--capabilities CAPABILITY_IAM
```
## Getting request info
To read information about the current request from the API Gateway event `(HTTP headers, HTTP method, body, path, ...)` or the current Lambda Context `(Function Name, Function Version, awsRequestId, time remaning, ...)`, use the options function. This way, they can be passed to your schema resolvers via the context option.
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ event, context }) => ({
headers: event.headers,
functionName: context.functionName,
event,
context,
})
});
exports.graphqlHandler = server.createHandler();
```
## Modifying the Lambda Response (Enable CORS)
To enable CORS, the response HTTP headers need to be modified. To accomplish this, use the `cors` options.
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
exports.graphqlHandler = server.createHandler({
cors: {
origin: '*',
credentials: true,
},
});
```
Furthermore, to enable CORS response for requests with credentials (cookies, http authentication), the `allow origin` and `credentials` header must be set to true.
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
exports.graphqlHandler = server.createHandler({
cors: {
origin: true,
credentials: true,
},
});
```

View file

@ -111,8 +111,6 @@ Your server itself is hosted at http://localhost:4000/graphql. This would be the
Depending on whether we are creating a new application or an existing application, the steps will vary slightly since Apollo Server must adapt to the semantics of existing servers (e.g. Express, Hapi, etc.)
Both import methods will use the `apollo-server` module we installed in the previous step, but existing applications will also install a middleware package which corresponds to the desired HTTP server.
<h3 id="middleware">Middleware</h3>
Existing applications generally already have middleware in place and Apollo Server works along with those middleware. To integrate with Apollo Server, we'll pass it into the `server.applyMiddleware` method as `app` to add the Apollo Server's middleware.
@ -136,6 +134,7 @@ app.listen({ port: 4000 }, () =>
)
```
Hapi follows the same pattern with `apollo-server-express` replaced with `apollo-server-hapi` and `app` replaced with Hapi server. `applyMiddleware` registers plugins, so it should be called with `await`.
<h3 id="serverless">Serverless</h3>

View file

@ -0,0 +1,74 @@
---
title: CDN Integration
description: Getting content delivery networks to cache GraphQL responses
---
Content-delivery networks such as [fly.io](https://fly.io), [Cloudflare](https://www.cloudflare.com/), [Akamai](https://www.akamai.com/) or [Fastly](https://www.fastly.com/) allow content caching close to clients, delivering data with low latency from a nearby server. Apollo Server makes it straightforward to use CDNs with GraphQL queries to cache full responses while still executing more dynamic queries.
To use Apollo Server behind a CDN, we define which GraphQL responses the CDN is allowed to cache. On the client, we set up [automatic persisted queries](./apq.html) to ensure that GraphQL requests are in a format that a CDN can understand.
<h2 id="cache-hints" title="1. Add cache hints">Step 1: Add cache hints to the GraphQL schema</h2>
Add cache hints as [directives](./directives.html) to GraphQL schema so that Apollo Server knows which fields and types are cacheable and for how long. For example, this schema indicates that all fields that return an `Author` should be cached for 60 seconds, and that the `posts` field should itself be cached for 180 seconds:
```graphql
type Author @cacheControl(maxAge: 60) {
id: Int
firstName: String
lastName: String
posts: [Post] @cacheControl(maxAge: 180)
}
```
See [the cache control documentation](https://github.com/apollographql/apollo-cache-control-js#add-cache-hints-to-your-schema) for more details, including how to specify hints dynamically inside resolvers, how to set a default `maxAge` for all fields, and how to specify that a field should be cached for specific users only (in which case CDNs should ignore it). For example, to set a default max age other than 0 modify the Apollo Server constructor to include `cacheControl`:
```js
const server = new ApolloServer({
typeDefs,
resolvers,
// The max age is calculated in seconds
cacheControl: { defaultMaxAge: 5 },
});
```
After this step, Apollo Server will serve the HTTP `Cache-Control` header on fully cacheable responses, so that any CDN in front of Apollo Server will know which responses can be cached and for how long! A "fully cacheable" response contains only data with non-zero `maxAge`; the header will refer to the minimum `maxAge` value across the whole response, and it will be `public` unless some of the data is tagged `scope: PRIVATE`. To observe this header, use any browser's network tab in its dev tools.
<h2 id="enable-apq" title="2. Enable persisted queries">Step 2: Enable automatic persisted queries</h2>
Often, GraphQL requests are big POST requests and most CDNs will only cache GET requests. Additionally, GET requests generally work best when the URL has a bounded size. Enabling automatic persisted queries means that short hashes are sent over the wire instead of full queries, and Apollo Client can be configured to use GET requests for those hashed queries.
To do this, update the **client** code. First, add the package:
```
npm install apollo-link-persisted-queries
```
Then, add the persisted queries link to the Apollo Client constructor before the HTTP link:
```js
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloLink } from "apollo-link";
import ApolloClient from "apollo-client";
ApolloLink.from([
createPersistedQueryLink({ useGETForHashedQueries: true }),
createHttpLink({ uri: "/graphql" })
]);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: link
});
```
Make sure to include `useGETForHashedQueries: true`. Note that the client will still use POSTs for mutations, because it's generally best to avoid GETs for non-idempotent requests.
If configured correctly, browser's dev tools should verify that queries are now sent as GET requests, and receive appropriate `Cache-Control` response headers.
<h2 id="setup-cdn" title="3. Set up a CDN">Step 3: Set up a CDN!</h2>
How exactly this works depends on exactly which CDN you chose. Configure your CDN to send requests to Apollo Server. Some CDNs may need to be specially configured to honor origin Cache-Control headers; for example, here is [Akamai's documentation on that setting](https://learn.akamai.com/en-us/webhelp/ion/oca/GUID-57C31126-F745-4FFB-AA92-6A5AAC36A8DA.html). If all is well, cacheable queries should now be saved by the CDN!
> Note that requests served directly by a CDN will not show up in the Engine dashboard.

View file

@ -1,51 +0,0 @@
---
title: Logging
description: Ensuring Actions can be Recreated
---
Apollo Server provides two ways to log a server: by input,response, and errors or periodically throughout a request's lifecycle. Treating the GraphQL execution as a black box by logging the inputs and outputs of the system allows developers to diagnose issues quickly without being mired by lower level logs. Once a problem has been found at a high level, the lower level logs enable accurate tracing of how a request was handled.
## High Level Logging
To log the inputs, response, and request, Apollo Server provides three methods: `formatParams`, `formatError`, and `formatResponse`. This example uses `console.log` to record the information, servers can use other more sophisticated tools.
```js
const server = new ApolloServer({
typeDefs,
resolvers,
formatParams: params => {
console.log(params);
return params;
},
formatError: error => {
console.log(error);
return error;
},
formatResponse: response => {
console.log(response);
return response;
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```
## Granular Logs
Apollo Server provides a `logFunction` option that receives the start and completion information for each major phase of GraphQL execution: parse, validate, and execute. Additionally, `logFunction` receives the information that initiates the request and response data. This example uses `console.log`:
```js
const server = new ApolloServer({
typeDefs,
resolvers,
logFunction: information => {
console.log(information)
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```

View file

@ -0,0 +1,70 @@
---
title: Monitoring and Metrics
description: How to monitor Apollo Server's performance
---
Understanding the behavior of GraphQL execution inside of Apollo Server is critical to developing and running a production GraphQL layer. Apollo Server enables GraphQL monitoring in Apollo Engine and provides more primitive native mechanisms to log each phase of a GraphQL request.
## Apollo Engine
Apollo Engine provides an integrated hub for all GraphQL performance data that is free for one million queries per month. With an API key from the [Engine UI](https://engine.apollographql.com/), Apollo Server reports performance and error data out-of-band. Apollo Engine then aggregates and displays information for [queries](https://www.apollographql.com/docs/engine/query-tracking.html), [requests](https://www.apollographql.com/docs/engine/performance.html), the [schema](https://www.apollographql.com/docs/engine/schema-analytics.html), and [errors](https://www.apollographql.com/docs/engine/error-tracking.html). In addition to aggregating data, Apollo Server provides [proactive alerts](https://www.apollographql.com/docs/engine/alerts.html), [daily slack reports](https://www.apollographql.com/docs/engine/reports.html), and [Datadog integration](https://www.apollographql.com/docs/engine/datadog.html).
To set up Apollo Server with Engine, [click here](https://engine.apollographql.com/) to get an Engine API key and provide it to the `ENGINE_API_KEY` environment variable. Setting an environment variable can be done in commandline as seen below or with the [dotenv npm package](https://www.npmjs.com/package/dotenv).
```bash
#Replace YOUR_API_KEY with the api key for you service in the Engine UI
ENGINE_API_KEY=YOUR_API_KEY node start-server.js
```
## Logging
Apollo Server provides two ways to log a server: per input, response, and errors or periodically throughout a request's lifecycle. Treating the GraphQL execution as a black box by logging the inputs and outputs of the system allows developers to diagnose issues quickly without being mired by lower level logs. Once a problem has been found at a high level, the lower level logs enable accurate tracing of how a request was handled.
### High Level Logging
To log the inputs, response, and request, Apollo Server provides three methods: `formatParams`, `formatError`, and `formatResponse`. This example uses `console.log` to record the information, servers can use other more sophisticated tools.
```js
const server = new ApolloServer({
typeDefs,
resolvers,
formatParams: params => {
console.log(params);
return params;
},
formatError: error => {
console.log(error);
return error;
},
formatResponse: response => {
console.log(response);
return response;
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```
### Granular Logs
Additionally for more advanced cases, Apollo Server accepts an array of `graphql-extensions` to the `extensions` field. These extensions receive a variety of lifecycle calls for each phase of a GraphQL request and can keep state, such as the request headers.
```js
const { ApolloServer } = require('apollo-server');
const LoggingExtension = require('./logging');
const server = new ApolloServer({
typeDefs,
resolvers,
extensions: [() => new LoggingExtension()]
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```
For example the `logFunction` from Apollo Server 1 can be implemented as an [extension](https://github.com/apollographql/apollo-server/blob/8914b135df9840051fe81cc9224b444cfc5b61ab/packages/apollo-server-core/src/logging.ts) and could be modified to add additional state or functionality. The example uses a beta of `graphql-extensions, which can be added to a project with `npm install graphql-extensions@beta`.

View file

@ -191,7 +191,7 @@ function oddValue(value) {
return value % 2 === 1 ? value : null;
}
const resovers = {
const resolvers = {
Odd: new GraphQLScalarType({
name: 'Odd',
description: 'Odd custom scalar type',
@ -236,7 +236,7 @@ type Query {
}
```
A query migh look like this:
A query might look like this:
```graphql
query {
@ -306,7 +306,7 @@ const resolvers = {
};
```
These don't change the public API at all and the resovers accept these value instead of the schema value, like so:
These don't change the public API at all and the resolvers accept these value instead of the schema value, like so:
```js
const resolvers = {

View file

@ -3,13 +3,11 @@ title: Using Engine with v2.0 RC
description: How to use Engine with Apollo Server 2.0 RC
---
Apollo Server provides reporting and persisted queries in native javascript by default, so often times moving to Apollo Server 2 without the Engine proxy is possible. For services that require the Engine proxy, Apollo Server continues to support with first class functionality. With Apollo Server 2, the engine proxy can be started by the same node process. If Engine is running in a dedicated machine, Apollo Server 2 supports the cache-control and tracing extensions, used to communicate with the proxy.
Apollo Server provides reporting, persisted queries, and cache-control headers in native javascript by default, so often times moving to Apollo Server 2 without the Engine proxy is possible. For services that already contain the Engine proxy and depend on its full response caching, Apollo Server continues to support it with first class functionality. With Apollo Server 2, the Engine proxy can be started by the same node process. If the Engine proxy is running in a dedicated machine, Apollo Server 2 supports the cache-control and tracing extensions, used to communicate with the proxy.
## Stand-alone Apollo Server
With ENGINE_API_KEY set as an environment variable, Apollo Server creates a reporting agent that sends execution traces to the Engine UI. In addition by default, Apollo Server supports [persisted queries](./features/apq.html).
<!-- FIXME add something about CDN headers-->
Apollo Server 2 is able to completely replace the Engine proxy. To enable metrics reporting, add `ENGINE_API_KEY` as an environment variable. Apollo Server will then create a reporting agent that sends execution traces to the Engine UI. In addition by default, Apollo Server supports [persisted queries](./features/apq.html) without needing the proxy's cache. Apollo Server also provides cache-control headers for consumption by a [CDN](./features/cdn.html). Integrating a CDN provides an alternative to the full response caching inside of Engine proxy.
```js
const { ApolloServer } = require('apollo-server');
@ -26,7 +24,7 @@ server.listen().then(({ url }) => {
## Starting Engine Proxy
The `apollo-engine` package provides integrations with many [node frameworks](/docs/engine/setup-node.html#not-express), including [express](/docs/engine/setup-node.html#setup-guide), that starts the Engine Proxy alongside the framework. The following code demonstrates how to start the proxy with Apollo Server 2, assuming that the `ENGINE_API_KEY` environment variable is set to the api key of the service.
Some infrastructure already contains the Engine proxy and requires it for full response caching, so it is necessary to run the proxy as a process alongside Apollo Server. If full response caching is not necessary, then the Engine proxy can be completely replaced by Apollo Server 2. The `apollo-engine` package provides integrations with many [node frameworks](/docs/engine/setup-node.html#not-express), including [express](/docs/engine/setup-node.html#setup-guide), and starts the Engine proxy alongside Apollo Server. The following code demonstrates how to start the proxy with Apollo Server 2. It assumes that the `ENGINE_API_KEY` environment variable is set to the api key of the service.
```js
const { ApolloEngine } = require('apollo-engine');
@ -38,7 +36,9 @@ const server = new ApolloServer({
typeDefs,
resolvers,
tracing: true,
cacheControl: true
cacheControl: true,
// We set `engine` to false, so that the new agent is not used.
engine: false,
});
server.applyMiddlware({ app });
@ -59,9 +59,26 @@ engine.listen({
});
```
To set the default max age inside of cacheControl, some additional options must be specified:
```js
const server = new ApolloServer({
typeDefs,
resolvers,
tracing: true,
cacheControl: {
defaultMaxAge: 5,
stripFormattedExtensions: false,
calculateCacheControlHeaders: false,
},
// We set `engine` to false, so that the new agent is not used.
engine: false,
});
```
## With a Running Engine Proxy
If the engine proxy is already running in a container in front of Apollo Server, then set `tracing` and `cacheControl` to true. These options will provide the extensions information to the proxy to create traces and ensure caching.
If the Engine proxy is already running in a container in front of Apollo Server, then set `tracing` and `cacheControl` to true. These options will provide the extensions information to the proxy to create traces and ensure caching. We set `engine` to false, so that the new metrics reporting pipeline is not activated.
```js
const { ApolloServer } = require('apollo-server');
@ -70,7 +87,9 @@ const server = new ApolloServer({
typeDefs,
resolvers,
tracing: true,
cacheControl: true
cacheControl: true,
// We set `engine` to false, so that the new agent is not used.
engine: false,
});
server.listen().then(({ url }) => {

View file

@ -25,10 +25,11 @@ const typeDefs = gql`
`;
//Some projects use schemas imported from external files
const typeDefs = gql`${IMPORT_FUNCTION('./schema-file')}`;
const fs = require('fs');
const typeDefs = gql`${fs.readFileSync(__dirname.concat('/schema.graphql'), 'utf8')}`;
//gql can also be used as regular function to convert a string to an AST
const typeDefs = gql(IMPORT_FUNCTION('./schema-file'))
const typeDefs = gql(fs.readFileSync(__dirname.concat('/schema.graphql'), 'utf8'))
```
<h2 id="app-deps">Changes to app dependencies</h2>
@ -192,9 +193,10 @@ app.listen({ port: 4000 }, () =>
For many existing instances of Apollo Server, the schema is created at runtime before server startup, using `makeExecutableSchema` or `mergeSchemas`. Apollo Server 2 stays backwards compatible with these more complex schemas, accepting it as the `schema` field in the server constructor options. Additionally, Apollo Server 2 exports all of `graphql-tools`, so `makeExecutableSchema` and other functions can be imported directly from Apollo Server.
> Note: the string to create these schema will not use th `gql` tag exported from apollo-server.
> Note: the string to create these schema will not use the `gql` tag exported from apollo-server.
```js
const { ApolloServer, makeExecutableSchema } = require('apollo-server');
const { ApolloServer, makeExecutableSchema, gql } = require('apollo-server');
//For developer tooling, such as autoformatting, use the following workaround
const gql = String.raw;
@ -209,7 +211,7 @@ const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
//mergeSchemas is imported from apollo-server
//mergeSchemas can be imported from apollo-server
//const schema = mergeSchemas(...);
const server = new ApolloServer({ schema });
@ -218,3 +220,70 @@ server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```
<h2 id="request-headers">Accessing Request Headers</h2>
Apollo Server 1 allowed request headers to be used in the construction of the GraphQL options. Apollo Server 2 allows constructor to create the context based upon the request.
```js
//old way
graphqlExpress((req, res) => ({
schema: myGraphQLSchema,
context: {
token: req.headers['auth-token'],
},
}))
//new way
new ApolloServer({
schema: myGraphQLSchema,
context: ({req, res}) => ({
token: req.headers['auth-token'],
}),
});
```
For most other functions that might have required access to the middleware arguments, such as `formatParams`, `formatError`, and `formatResponse`, it is possible to create a `graphql-extension`.
For more complicated use cases, the `ApolloServer` class can be extended to override the `createGraphQLServerOptions` function to create parameters based on the same argument that's passed to the context.
<h2 id="log-function">Replacing `logFunction`</h2>
Apollo Server 2 removes the `logFunction` in favor of using `graphql-extensions`, which provides a more structured and flexible way of instrumenting Apollo Server. The explanation of how to do this more granular logging, can be found in the [metrics section](./features/metrics.html)
<h2 id="graphiql">Replacing GraphiQL</h2>
Apollo Server 2 ships with GraphQL Playground instead of GraphiQL and collocates the gui with the endpoint. GraphQL playground can be customized in the following manner.
```js
const { ApolloServer, gql } = require('apollo-server-express');
const server = new ApolloServer({
// These will be defined for both new or existing servers
typeDefs,
resolvers,
});
server.applyMiddleware({
app, // app is from an existing express app
gui: {
endpoint?: string
subscriptionEndpoint?: string
tabs: [
{
endpoint: string
query: string
variables?: string
responses?: string[]
headers?: { [key: string]: string }
},
],
}
});
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
```
Some Apollo Server 1 implementations use a custom version of GraphiQL, which can be added to Apollo Server 2 as a middleware or ported to use the [React version of GraphQL Playground](https://www.npmjs.com/package/graphql-playground-react).

View file

@ -116,7 +116,7 @@ server.listen().then(({ url }) => {
## Health Checks
The default Apollo server provides a health check endpoint at `/.well-known/apollo/server-health` hat returns a 200 status code by default. If `onHealthCheck` is defined, the promise returned from the callback determines the status code. A successful resolution causes a 200 and rejection causes a 503. Health checks are often used by load balancers to determine if a server is available.
The default Apollo server provides a health check endpoint at `/.well-known/apollo/server-health` that returns a 200 status code by default. If `onHealthCheck` is defined, the promise returned from the callback determines the status code. A successful resolution causes a 200 and rejection causes a 503. Health checks are often used by load balancers to determine if a server is available.
```js
const { ApolloServer, gql } = require('apollo-server');

View file

@ -1,6 +1,6 @@
{
"lerna": "2.0.0",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"changelog": {
"repo": "apollographql/apollo-server",
"labels": {

View file

@ -1,6 +1,6 @@
{
"name": "apollo-datasource-rest",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
@ -23,8 +23,9 @@
"node": ">=6"
},
"dependencies": {
"apollo-server-env": "^2.0.0-rc.0",
"apollo-server-caching": "^2.0.0-rc.0",
"apollo-server-caching": "^2.0.0-rc.3",
"apollo-server-env": "^2.0.0-rc.3",
"apollo-server-errors": "^2.0.0-rc.3",
"http-cache-semantics": "^4.0.0",
"lru-cache": "^4.1.3"
},

View file

@ -1,4 +1,4 @@
import CachePolicy from 'http-cache-semantics';
import CachePolicy = require('http-cache-semantics');
import { KeyValueCache, InMemoryLRUCache } from 'apollo-server-caching';
@ -69,7 +69,13 @@ export class HTTPCache {
const body = await response.text();
const entry = JSON.stringify({ policy: policy.toObject(), body });
await this.keyValueCache.set(cacheKey, entry);
let ttl = policy.timeToLive() / 1000;
// If a response can be revalidated, we don't want to remove it from the cache right after it expires.
// We may be able to use better heuristics here, but for now we'll take the max-age times 2.
if (canBeRevalidated(response)) {
ttl *= 2;
}
await this.keyValueCache.set(cacheKey, entry, { ttl });
// We have to clone the response before returning it because the
// body can only be used once.
@ -83,6 +89,10 @@ export class HTTPCache {
}
}
function canBeRevalidated(response: Response): boolean {
return response.headers.has('ETag');
}
function cacheKeyFor(request: Request): string {
// FIXME: Find a way to take Vary header fields into account when computing a cache key
// Although we do validate header fields and don't serve responses from cache when they don't match,

View file

@ -1,4 +1,9 @@
import { HTTPCache } from './HTTPCache';
import {
ApolloError,
AuthenticationError,
ForbiddenError,
} from 'apollo-server-errors';
export abstract class RESTDataSource<TContext = any> {
abstract baseURL: string;
@ -6,73 +11,89 @@ export abstract class RESTDataSource<TContext = any> {
httpCache!: HTTPCache;
context!: TContext;
public willSendRequest?(request: Request): void;
protected willSendRequest?(request: Request): void;
protected async get<TResponse = any>(
protected async didReceiveErrorResponse<TResult = any>(
response: Response,
): Promise<TResult> {
const message = `${response.status} ${
response.statusText
}: ${await response.text()}`;
if (response.status === 401) {
throw new AuthenticationError(message);
} else if (response.status === 403) {
throw new ForbiddenError(message);
} else {
throw new ApolloError(message);
}
}
protected async get<TResult = any>(
path: string,
params?: URLSearchParamsInit,
options?: RequestInit,
): Promise<TResponse> {
return this.fetch<TResponse>(
): Promise<TResult> {
return this.fetch<TResult>(
path,
params,
Object.assign({ method: 'GET' }, options),
);
}
protected async post<TResponse = any>(
protected async post<TResult = any>(
path: string,
params?: URLSearchParamsInit,
options?: RequestInit,
): Promise<TResponse> {
return this.fetch<TResponse>(
): Promise<TResult> {
return this.fetch<TResult>(
path,
params,
Object.assign({ method: 'POST' }, options),
);
}
protected async patch<TResponse = any>(
protected async patch<TResult = any>(
path: string,
params?: URLSearchParamsInit,
options?: RequestInit,
): Promise<TResponse> {
return this.fetch<TResponse>(
): Promise<TResult> {
return this.fetch<TResult>(
path,
params,
Object.assign({ method: 'PATCH' }, options),
);
}
protected async put<TResponse = any>(
protected async put<TResult = any>(
path: string,
params?: URLSearchParamsInit,
options?: RequestInit,
): Promise<TResponse> {
return this.fetch<TResponse>(
): Promise<TResult> {
return this.fetch<TResult>(
path,
params,
Object.assign({ method: 'PUT' }, options),
);
}
protected async delete<TResponse = any>(
protected async delete<TResult = any>(
path: string,
params?: URLSearchParamsInit,
options?: RequestInit,
): Promise<TResponse> {
return this.fetch<TResponse>(
): Promise<TResult> {
return this.fetch<TResult>(
path,
params,
Object.assign({ method: 'DELETE' }, options),
);
}
private async fetch<TResponse>(
private async fetch<TResult>(
path: string,
params?: URLSearchParamsInit,
init?: RequestInit,
): Promise<TResponse> {
): Promise<TResult> {
const url = new URL(path, this.baseURL);
if (params) {
@ -99,17 +120,15 @@ export abstract class RESTDataSource<TContext = any> {
return response.text();
}
} else {
throw new Error(
`${response.status} ${response.statusText}: ${await response.text()}`,
);
return this.didReceiveErrorResponse(response);
}
});
}
private async trace<Result>(
private async trace<TResult>(
label: string,
fn: () => Promise<Result>,
): Promise<Result> {
fn: () => Promise<TResult>,
): Promise<TResult> {
if (process && process.env && process.env.NODE_ENV === 'development') {
// We're not using console.time because that isn't supported on Cloudflare
const startTime = Date.now();

View file

@ -21,14 +21,7 @@ describe('HTTPCache', () => {
fetch.mockReset();
store = new Map();
httpCache = new HTTPCache({
async get(key: string) {
return store.get(key);
},
async set(key: string, value: string) {
store.set(key, value);
},
});
httpCache = new HTTPCache(store as any);
});
afterAll(() => {
@ -157,6 +150,36 @@ describe('HTTPCache', () => {
expect(await response.json()).toEqual({ name: 'Alan Turing' });
});
it('sets the TTL as max-age when the response does not contain revalidation headers', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{ 'Cache-Control': 'max-age=30' },
);
const storeSet = jest.spyOn(store, 'set');
await httpCache.fetch('https://api.example.com/people/1');
expect(storeSet.mock.calls[0][2]).toEqual({ ttl: 30 });
storeSet.mockRestore();
});
it('sets the TTL as 2 * max-age when the response contains an ETag header', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{ 'Cache-Control': 'max-age=30', ETag: 'foo' },
);
const storeSet = jest.spyOn(store, 'set');
await httpCache.fetch('https://api.example.com/people/1');
expect(storeSet.mock.calls[0][2]).toEqual({ ttl: 60 });
storeSet.mockRestore();
});
it('revalidates a cached response when expired and returns the cached response when not modified', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },

View file

@ -1,4 +1,9 @@
import 'apollo-server-env';
import {
ApolloError,
AuthenticationError,
ForbiddenError,
} from 'apollo-server-errors';
import { RESTDataSource } from '../RESTDataSource';
import fetch, { mockFetch, unmockFetch } from '../../../../__mocks__/fetch';
@ -178,4 +183,52 @@ describe('RESTDataSource', () => {
expect(fetch.mock.calls[0][0].method).toEqual(method);
});
}
it('throws an AuthenticationError when the response status is 401', async () => {
const dataSource = new class extends RESTDataSource {
baseURL = 'https://api.example.com';
getFoo() {
return this.get('foo');
}
}();
dataSource.httpCache = httpCache;
fetch.mockResponseOnce('Invalid token', undefined, 401);
await expect(dataSource.getFoo()).rejects.toThrow(AuthenticationError);
});
it('throws a ForbiddenError when the response status is 403', async () => {
const dataSource = new class extends RESTDataSource {
baseURL = 'https://api.example.com';
getFoo() {
return this.get('foo');
}
}();
dataSource.httpCache = httpCache;
fetch.mockResponseOnce('No access', undefined, 403);
await expect(dataSource.getFoo()).rejects.toThrow(ForbiddenError);
});
it('throws an ApolloError when the response status is 500', async () => {
const dataSource = new class extends RESTDataSource {
baseURL = 'https://api.example.com';
getFoo() {
return this.get('foo');
}
}();
dataSource.httpCache = httpCache;
fetch.mockResponseOnce('Oops', undefined, 500);
await expect(dataSource.getFoo()).rejects.toThrow(ApolloError);
});
});

View file

@ -20,6 +20,8 @@ declare module 'http-cache-semantics' {
satisfiesWithoutRevalidation(request: Request): boolean;
responseHeaders(): Headers;
timeToLive(): number;
revalidationHeaders(request: Request): Headers;
revalidatedPolicy(
request: Request,

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-caching",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {

View file

@ -1,4 +1,4 @@
import LRU from 'lru-cache';
import * as LRU from 'lru-cache';
import { KeyValueCache } from './KeyValueCache';
export class InMemoryLRUCache implements KeyValueCache {

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-cloudflare",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"description": "Production-ready Node.js GraphQL server for Cloudflare workers",
"main": "dist/index.js",
"scripts": {
@ -24,7 +24,10 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^2.0.0-rc.0"
"apollo-server-core": "^2.0.0-rc.3"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"typings": "dist/index.d.ts",
"typescript": {

View file

@ -5,9 +5,9 @@ export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';
import { GraphQLOptions } from 'apollo-server-core';
export class ApolloServer extends ApolloServerBase {
//This translates the arguments from the middleware into graphQL options It
//provides typings for the integration specific behavior, ideally this would
//be propagated with a generic to the super class
// This translates the arguments from the middleware into graphQL options It
// provides typings for the integration specific behavior, ideally this would
// be propagated with a generic to the super class
async createGraphQLServerOptions(request: Request): Promise<GraphQLOptions> {
return super.graphQLServerOptions({ request });
}

View file

@ -38,11 +38,8 @@ export function graphqlCloudflare(options: GraphQLOptions) {
query,
request: req as Request,
}).then(
gqlResponse =>
new Response(gqlResponse, {
status: 200,
headers: { 'content-type': 'application/json' },
}),
({ graphqlResponse, responseInit }) =>
new Response(graphqlResponse, responseInit),
(error: HttpQueryError) => {
if ('HttpQueryError' !== error.name) throw error;

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-core",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"description": "Core engine for Apollo GraphQL server",
"main": "dist/index.js",
"scripts": {
@ -27,13 +27,31 @@
"engines": {
"node": ">=6"
},
"dependencies": {
"@types/ws": "^5.1.2",
"apollo-cache-control": "^0.1.1",
"apollo-datasource-rest": "^2.0.0-rc.3",
"apollo-engine-reporting": "0.0.0-beta.15",
"apollo-server-env": "^2.0.0-rc.3",
"apollo-server-errors": "^2.0.0-rc.3",
"apollo-tracing": "^0.2.0-beta.1",
"apollo-upload-server": "^5.0.0",
"graphql-extensions": "0.1.0-beta.15",
"graphql-subscriptions": "^0.5.8",
"graphql-tag": "^2.9.2",
"graphql-tools": "^3.0.2",
"hash.js": "^1.1.3",
"keyv": "^3.0.0",
"lodash": "^4.17.10",
"quick-lru": "^1.1.0",
"subscriptions-transport-ws": "^0.9.9",
"ws": "^5.2.0"
},
"devDependencies": {
"@types/fibers": "0.0.30",
"@types/graphql": "^0.13.1",
"@types/keyv": "^3.0.1",
"@types/node-fetch": "^2.1.1",
"@types/quick-lru": "^1.1.0",
"@types/ws": "^5.1.2",
"apollo-fetch": "^0.7.0",
"apollo-link": "^1.2.2",
"apollo-link-http": "^1.5.4",
@ -49,22 +67,5 @@
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
},
"dependencies": {
"apollo-cache-control": "^0.1.1",
"apollo-datasource-rest": "^2.0.0-rc.0",
"apollo-engine-reporting": "0.0.0-beta.12",
"apollo-server-env": "^2.0.0-rc.0",
"apollo-tracing": "^0.2.0-beta.1",
"graphql-extensions": "0.1.0-beta.13",
"graphql-subscriptions": "^0.5.8",
"graphql-tag": "^2.9.2",
"graphql-tools": "^3.0.2",
"hash.js": "^1.1.3",
"keyv": "^3.0.0",
"node-fetch": "^2.1.2",
"quick-lru": "^1.1.0",
"subscriptions-transport-ws": "^0.9.9",
"ws": "^5.2.0"
}
}

View file

@ -1,8 +1,4 @@
import {
makeExecutableSchema,
addMockFunctionsToSchema,
mergeSchemas,
} from 'graphql-tools';
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { Server as HttpServer } from 'http';
import {
execute,
@ -18,27 +14,29 @@ import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingAgent } from 'apollo-engine-reporting';
import { InMemoryLRUCache } from 'apollo-datasource-rest';
import { GraphQLUpload } from 'apollo-upload-server';
import {
SubscriptionServer,
ExecutionParams,
} from 'subscriptions-transport-ws';
//use as default persisted query store
// use as default persisted query store
import Keyv = require('keyv');
import QuickLru = require('quick-lru');
import { formatApolloErrors } from './errors';
import { formatApolloErrors } from 'apollo-server-errors';
import {
GraphQLServerOptions as GraphQLOptions,
PersistedQueryOptions,
} from './graphqlOptions';
import { LogFunction } from './logging';
import {
Config,
Context,
ContextFunction,
SubscriptionServerOptions,
FileUploadOptions,
} from './types';
import { gql } from './index';
@ -66,11 +64,12 @@ export class ApolloServerBase {
private engineReportingAgent?: EngineReportingAgent;
private extensions: Array<() => GraphQLExtension>;
protected subscriptionServerOptions?: SubscriptionServerOptions;
protected uploadsConfig?: FileUploadOptions;
// set by installSubscriptionHandlers.
private subscriptionServer?: SubscriptionServer;
//The constructor should be universal across all environments. All environment specific behavior should be set in an exported registerServer or in by overriding listen
// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
constructor(config: Config) {
if (!config) throw new Error('ApolloServer requires options.');
const {
@ -84,14 +83,15 @@ export class ApolloServerBase {
extensions,
engine,
subscriptions,
uploads,
...requestOptions
} = config;
//While reading process.env is slow, a server should only be constructed
//once per run, so we place the env check inside the constructor. If env
//should be used outside of the constructor context, place it as a private
//or protected field of the class instead of a global. Keeping the read in
//the contructor enables testing of different environments
// While reading process.env is slow, a server should only be constructed
// once per run, so we place the env check inside the constructor. If env
// should be used outside of the constructor context, place it as a private
// or protected field of the class instead of a global. Keeping the read in
// the contructor enables testing of different environments
const isDev = process.env.NODE_ENV !== 'production';
// if this is local dev, introspection should turned on
@ -109,16 +109,16 @@ export class ApolloServerBase {
if (requestOptions.persistedQueries !== false) {
if (!requestOptions.persistedQueries) {
//maxSize is the number of elements that can be stored inside of the cache
//https://github.com/withspectrum/spectrum has about 200 instances of gql`
//300 queries seems reasonable
// maxSize is the number of elements that can be stored inside of the cache
// https://github.com/withspectrum/spectrum has about 200 instances of gql`
// 300 queries seems reasonable
const lru = new QuickLru({ maxSize: 300 });
requestOptions.persistedQueries = {
cache: new Keyv({ store: lru }),
};
}
} else {
//the user does not want to use persisted queries, so we remove the field
// the user does not want to use persisted queries, so we remove the field
delete requestOptions.persistedQueries;
}
@ -129,16 +129,41 @@ export class ApolloServerBase {
this.requestOptions = requestOptions as GraphQLOptions;
this.context = context;
if (uploads !== false) {
if (this.supportsUploads()) {
if (uploads === true || typeof uploads === 'undefined') {
this.uploadsConfig = {};
} else {
this.uploadsConfig = uploads;
}
//This is here to check if uploads is requested without support. By
//default we enable them if supported by the integration
} else if (uploads) {
throw new Error(
'This implementation of ApolloServer does not support file uploads because the environmnet cannot accept multi-part forms',
);
}
}
//Add upload resolver
if (this.uploadsConfig) {
if (resolvers && !resolvers.Upload) {
resolvers.Upload = GraphQLUpload;
}
}
this.schema = schema
? schema
: makeExecutableSchema({
//we add in the upload scalar, so that schemas that don't include it
//won't error when we makeExecutableSchema
typeDefs: [
gql`
scalar Upload
`,
].concat(typeDefs),
// we add in the upload scalar, so that schemas that don't include it
// won't error when we makeExecutableSchema
typeDefs: this.uploadsConfig
? [
gql`
scalar Upload
`,
].concat(typeDefs)
: typeDefs,
schemaDirectives,
resolvers,
});
@ -152,7 +177,7 @@ export class ApolloServerBase {
}
// Note: doRunQuery will add its own extensions if you set tracing,
// cacheControl, or logFunction.
// or cacheControl.
this.extensions = [];
if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
@ -183,6 +208,9 @@ export class ApolloServerBase {
}
// This is part of the public API.
this.subscriptionsPath = this.subscriptionServerOptions.path;
//This is here to check if subscriptions are requested without support. By
//default we enable them if supported by the integration
} else if (subscriptions) {
throw new Error(
'This implementation of ApolloServer does not support GraphQL subscriptions.',
@ -191,20 +219,12 @@ export class ApolloServerBase {
}
}
//used by integrations to synchronize the path with subscriptions, some
//integrations do not have paths, such as lambda
// used by integrations to synchronize the path with subscriptions, some
// integrations do not have paths, such as lambda
public setGraphQLPath(path: string) {
this.graphqlPath = path;
}
// If this is more generally useful to things other than Upload, we can make
// it public.
protected enhanceSchema(schema: GraphQLSchema) {
this.schema = mergeSchemas({
schemas: [this.schema, schema],
});
}
public async stop() {
if (this.subscriptionServer) await this.subscriptionServer.close();
if (this.engineReportingAgent) {
@ -250,7 +270,6 @@ export class ApolloServerBase {
formatApolloErrors([...value.errors], {
formatter: this.requestOptions.formatError,
debug: this.requestOptions.debug,
logFunction: this.requestOptions.logFunction,
}),
});
let context: Context = this.context ? this.context : { connection };
@ -264,7 +283,6 @@ export class ApolloServerBase {
throw formatApolloErrors([e], {
formatter: this.requestOptions.formatError,
debug: this.requestOptions.debug,
logFunction: this.requestOptions.logFunction,
})[0];
}
@ -283,9 +301,13 @@ export class ApolloServerBase {
return false;
}
//This function is used by the integrations to generate the graphQLOptions
//from an object containing the request and other integration specific
//options
protected supportsUploads(): boolean {
return false;
}
// This function is used by the integrations to generate the graphQLOptions
// from an object containing the request and other integration specific
// options
protected async graphQLServerOptions(
integrationContextArgument?: Record<string, any>,
) {
@ -297,7 +319,7 @@ export class ApolloServerBase {
? await this.context(integrationContextArgument || {})
: context;
} catch (error) {
//Defer context error resolution to inside of runQuery
// Defer context error resolution to inside of runQuery
context = () => {
throw error;
};
@ -310,7 +332,6 @@ export class ApolloServerBase {
// Allow overrides from options. Be explicit about a couple of them to
// avoid a bad side effect of the otherwise useful noUnusedLocals option
// (https://github.com/Microsoft/TypeScript/issues/21673).
logFunction: this.requestOptions.logFunction as LogFunction,
persistedQueries: this.requestOptions
.persistedQueries as PersistedQueryOptions,
fieldResolver: this.requestOptions.fieldResolver as GraphQLFieldResolver<

View file

@ -1,4 +1,77 @@
import { ExecutionResult } from 'graphql';
import { CacheControlFormat } from 'apollo-cache-control';
export interface PersistedQueryCache {
set(key: string, data: string): Promise<any>;
get(key: string): Promise<string | null>;
}
export type HttpHeaderCalculation = (
responses: Array<ExecutionResult & { extensions?: Record<string, any> }>,
) => Record<string, string>;
export function calculateCacheControlHeaders(
responses: Array<ExecutionResult & { extensions?: Record<string, any> }>,
): Record<string, string> {
let lowestMaxAge = Number.MAX_VALUE;
let publicOrPrivate = 'public';
// Because of the early exit, we are unable to use forEach. While a reduce
// loop might be possible, a for loop is more readable
for (let i = 0; i < responses.length; i++) {
const response = responses[i];
const cacheControl: CacheControlFormat =
response.extensions && response.extensions.cacheControl;
// If there are no extensions or hints, then the headers should not be present
if (
!cacheControl ||
!cacheControl.hints ||
cacheControl.hints.length === 0 ||
cacheControl.version !== 1
) {
if (cacheControl && cacheControl.version !== 1) {
console.warn('Invalid cacheControl version.');
}
return {};
}
const rootHints = new Set<string>();
for (let j = 0; j < cacheControl.hints.length; j++) {
const hint = cacheControl.hints[j];
if (hint.scope && hint.scope.toLowerCase() === 'private') {
publicOrPrivate = 'private';
}
// If no maxAge is present, then we ignore the hint
if (hint.maxAge === undefined) {
continue;
}
// if there is a hint with max age of 0, we don't need to process more
if (hint.maxAge === 0) {
return {};
}
if (hint.maxAge < lowestMaxAge) {
lowestMaxAge = hint.maxAge;
}
// If this is a root path, store that the root is cacheable:
if (hint.path.length === 1) {
rootHints.add(hint.path[0] as string);
}
}
// If a root field inside of data does not have a cache hint, then we do not
// cache the response
if (Object.keys(response.data).find(rootKey => !rootHints.has(rootKey))) {
return {};
}
}
return {
'Cache-Control': `max-age=${lowestMaxAge}, ${publicOrPrivate}`,
};
}

View file

@ -13,7 +13,7 @@ import {
ValidationError,
UserInputError,
SyntaxError,
} from './errors';
} from 'apollo-server-errors';
describe('Errors', () => {
describe('ApolloError', () => {
@ -106,19 +106,6 @@ describe('Errors', () => {
'stacktrace should exist under exception',
).not.to.exist;
});
it('calls logFunction with each error', () => {
const error = new ApolloError(message, code, { key });
const logFunction = stub();
formatApolloErrors([error], {
logFunction,
debug: true,
});
expect(error.message).to.equal(message);
expect(error.key).to.equal(key);
expect(error.extensions.code).to.equal(code);
expect(error instanceof ApolloError).true;
expect(logFunction.calledOnce);
});
it('calls formatter after exposing the code and stacktrace', () => {
const error = new ApolloError(message, code, { key });
const formatter = stub();

View file

@ -3,10 +3,10 @@ import {
ValidationContext,
GraphQLFieldResolver,
} from 'graphql';
import { LogFunction } from './logging';
import { PersistedQueryCache } from './caching';
import { PersistedQueryCache, HttpHeaderCalculation } from './caching';
import { GraphQLExtension } from 'graphql-extensions';
import { RESTDataSource, KeyValueCache } from 'apollo-datasource-rest';
import { CacheControlExtensionOptions } from 'apollo-cache-control';
/*
* GraphQLServerOptions
@ -15,7 +15,6 @@ import { RESTDataSource, KeyValueCache } from 'apollo-datasource-rest';
* - (optional) formatError: Formatting function applied to all errors before response is sent
* - (optional) rootValue: rootValue passed to GraphQL execution
* - (optional) context: the context passed to GraphQL execution
* - (optional) logFunction: a function called for logging events such as execution times
* - (optional) formatParams: a function applied to the parameters of every invocation of runQuery
* - (optional) validationRules: extra validation rules applied to requests
* - (optional) formatResponse: a function applied to each graphQL execution result
@ -33,15 +32,18 @@ export interface GraphQLServerOptions<
formatError?: Function;
rootValue?: any;
context?: TContext;
logFunction?: LogFunction;
formatParams?: Function;
validationRules?: Array<(context: ValidationContext) => any>;
formatResponse?: Function;
fieldResolver?: GraphQLFieldResolver<any, TContext>;
debug?: boolean;
tracing?: boolean;
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
cacheControl?:
| boolean
| (CacheControlExtensionOptions & {
calculateHttpHeaders?: boolean | HttpHeaderCalculation;
stripFormattedExtensions?: boolean;
});
extensions?: Array<() => GraphQLExtension>;
dataSources?: () => DataSources;
cache?: KeyValueCache;

View file

@ -1,6 +1,5 @@
import 'apollo-server-env';
export { runQuery } from './runQuery';
export { LogFunction, LogMessage, LogStep, LogAction } from './logging';
export { runHttpQuery, HttpQueryRequest, HttpQueryError } from './runHttpQuery';
export {
default as GraphQLOptions,
@ -16,7 +15,7 @@ export {
ForbiddenError,
UserInputError,
formatApolloErrors,
} from './errors';
} from 'apollo-server-errors';
export { convertNodeHttpToRequest } from './nodeHttpToRequest';
@ -27,11 +26,11 @@ export * from './types';
export * from 'graphql-tools';
export * from 'graphql-subscriptions';
//This currently provides the ability to have syntax highlighting as well as
//consistency between client and server gql tags
// This currently provides the ability to have syntax highlighting as well as
// consistency between client and server gql tags
import { DocumentNode } from 'graphql';
import gqlTag from 'graphql-tag';
export const gql: (
template: TemplateStringsArray,
template: TemplateStringsArray | string,
...substitutions: any[]
) => DocumentNode = gqlTag;

View file

@ -1,106 +0,0 @@
import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
import { print, DocumentNode } from 'graphql';
export enum LogAction {
request,
parse,
validation,
execute,
setup,
cleanup,
}
export enum LogStep {
start,
end,
status,
}
export interface LogMessage {
action: LogAction;
step: LogStep;
key?: string;
data?: any;
}
export interface LogFunction {
(message: LogMessage);
}
// A GraphQLExtension that implements the existing logFunction interface. Note
// that now that custom extensions are supported, you may just want to do your
// logging as a GraphQLExtension rather than write a LogFunction.
export class LogFunctionExtension<TContext = any>
implements GraphQLExtension<TContext> {
private logFunction: LogFunction;
public constructor(logFunction: LogFunction) {
this.logFunction = logFunction;
}
public requestDidStart(options: {
request: Request;
queryString?: string;
parsedQuery?: DocumentNode;
operationName?: string;
variables?: { [key: string]: any };
}) {
this.logFunction({ action: LogAction.request, step: LogStep.start });
const loggedQuery = options.queryString || print(options.parsedQuery);
this.logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'query',
data: loggedQuery,
});
this.logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'variables',
data: options.variables,
});
this.logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'operationName',
data: options.operationName,
});
return (...errors: Array<Error>) => {
// If there are no errors, we log in willSendResponse instead.
if (errors.length) {
this.logFunction({ action: LogAction.request, step: LogStep.end });
}
};
}
public parsingDidStart() {
this.logFunction({ action: LogAction.parse, step: LogStep.start });
return () => {
this.logFunction({ action: LogAction.parse, step: LogStep.end });
};
}
public validationDidStart() {
this.logFunction({ action: LogAction.validation, step: LogStep.start });
return () => {
this.logFunction({ action: LogAction.validation, step: LogStep.end });
};
}
public executionDidStart() {
this.logFunction({ action: LogAction.execute, step: LogStep.start });
return () => {
this.logFunction({ action: LogAction.execute, step: LogStep.end });
};
}
public willSendResponse(o: { graphqlResponse: GraphQLResponse }) {
this.logFunction({
action: LogAction.request,
step: LogStep.end,
key: 'response',
data: o.graphqlResponse,
});
}
}

View file

@ -1,5 +1,4 @@
import { IncomingMessage } from 'http';
import { Request, Headers } from 'node-fetch';
export function convertNodeHttpToRequest(req: IncomingMessage): Request {
const headers = new Headers();

View file

@ -1,7 +1,7 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai';
import 'mocha';
import MockReq from 'mock-req';
import * as MockReq from 'mock-req';
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

View file

@ -1,5 +1,10 @@
import { ExecutionResult } from 'graphql';
import sha256 from 'hash.js/lib/hash/sha/256';
import * as sha256 from 'hash.js/lib/hash/sha/256';
import { HTTPCache } from 'apollo-datasource-rest';
import { CacheControlExtensionOptions } from 'apollo-cache-control';
import { omit } from 'lodash';
import { runQuery, QueryOptions } from './runQuery';
import {
@ -10,9 +15,8 @@ import {
formatApolloErrors,
PersistedQueryNotSupportedError,
PersistedQueryNotFoundError,
} from './errors';
import { LogAction, LogStep } from './logging';
import { HTTPCache } from 'apollo-datasource-rest';
} from 'apollo-server-errors';
import { calculateCacheControlHeaders, HttpHeaderCalculation } from './caching';
export interface HttpQueryRequest {
method: string;
@ -28,11 +32,23 @@ export interface HttpQueryRequest {
request: Pick<Request, 'url' | 'method' | 'headers'>;
}
//The result of a curl does not appear well in the terminal, so we add an extra new line
// The result of a curl does not appear well in the terminal, so we add an extra new line
function prettyJSONStringify(toStringfy) {
return JSON.stringify(toStringfy) + '\n';
}
export interface ApolloServerHttpResponse {
headers?: Record<string, string>;
// ResponseInit contains the follow, which we do not use
// status?: number;
// statusText?: string;
}
export interface HttpQueryResponse {
graphqlResponse: string;
responseInit: ApolloServerHttpResponse;
}
export class HttpQueryError extends Error {
public statusCode: number;
public isGraphQLError: boolean;
@ -63,7 +79,6 @@ function throwHttpGraphQLError(
errors: formatApolloErrors(errors, {
debug: optionsObject.debug,
formatter: optionsObject.formatError,
logFunction: optionsObject.logFunction,
}),
}),
true,
@ -76,11 +91,15 @@ function throwHttpGraphQLError(
export async function runHttpQuery(
handlerArguments: Array<any>,
request: HttpQueryRequest,
): Promise<string> {
): Promise<HttpQueryResponse> {
let isGetRequest: boolean = false;
let optionsObject: GraphQLOptions;
const debugDefault =
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
let cacheControl: CacheControlExtensionOptions & {
calculateHttpHeaders: boolean | HttpHeaderCalculation;
stripFormattedExtensions: boolean;
};
try {
optionsObject = await resolveGraphqlOptions(
@ -90,7 +109,7 @@ export async function runHttpQuery(
} catch (e) {
// The options can be generated asynchronously, so we don't have access to
// the normal options provided by the user, such as: formatError,
// logFunction, debug. Therefore, we need to do some unnatural things, such
// debug. Therefore, we need to do some unnatural things, such
// as use NODE_ENV to determine the debug settings
e.message = `Invalid options provided to ApolloServer: ${e.message}`;
if (!debugDefault) {
@ -146,6 +165,8 @@ export async function runHttpQuery(
try {
let queryString: string | undefined = requestParams.query;
let extensions = requestParams.extensions;
let persistedQueryHit = false;
let persistedQueryRegister = false;
if (isGetRequest && extensions) {
// For GET requests, we have to JSON-parse extensions. (For POST
@ -168,6 +189,11 @@ export async function runHttpQuery(
!optionsObject.persistedQueries ||
!optionsObject.persistedQueries.cache
) {
if (isBatch) {
// A batch can contain another query that returns data,
// so we don't error out the entire request with an HttpError
throw new PersistedQueryNotSupportedError();
}
// Return 200 to simplify processing: we want this to be intepreted by
// the client as data worth interpreting, not an error.
throwHttpGraphQLError(
@ -183,7 +209,14 @@ export async function runHttpQuery(
if (queryString === undefined) {
queryString = await optionsObject.persistedQueries.cache.get(sha);
if (!queryString) {
if (queryString) {
persistedQueryHit = true;
} else {
if (isBatch) {
// A batch can contain multiple undefined persisted queries,
// so we don't error out the entire request with an HttpError
throw new PersistedQueryNotFoundError();
}
throwHttpGraphQLError(
200,
[new PersistedQueryNotFoundError()],
@ -197,29 +230,21 @@ export async function runHttpQuery(
if (sha !== calculatedSha) {
throw new HttpQueryError(400, 'provided sha does not match query');
}
persistedQueryRegister = true;
//Do the store completely asynchronously
// Do the store completely asynchronously
Promise.resolve()
.then(() => {
//We do not wait on the cache storage to complete
// We do not wait on the cache storage to complete
return optionsObject.persistedQueries.cache.set(sha, queryString);
})
.catch(error => {
if (optionsObject.logFunction) {
optionsObject.logFunction({
action: LogAction.setup,
step: LogStep.status,
key: 'error',
data: error,
});
} else {
console.warn(error);
}
console.warn(error);
});
}
}
//We ensure that there is a queryString or parsedQuery after formatParams
// We ensure that there is a queryString or parsedQuery after formatParams
if (queryString && typeof queryString !== 'string') {
// Check for a common error first.
if (queryString && (queryString as any).kind === 'Document') {
@ -269,7 +294,7 @@ export async function runHttpQuery(
let context = optionsObject.context;
if (!context) {
//appease typescript compiler, otherwise could use || {}
// appease typescript compiler, otherwise could use || {}
context = {};
} else if (typeof context === 'function') {
try {
@ -288,7 +313,7 @@ export async function runHttpQuery(
if (optionsObject.dataSources) {
const dataSources = optionsObject.dataSources() || {};
//we use the cache provided to the request and add the Http semantics on top
// we use the cache provided to the request and add the Http semantics on top
const httpCache = new HTTPCache(optionsObject.cache);
for (const dataSource of Object.values(dataSources)) {
@ -305,6 +330,31 @@ export async function runHttpQuery(
(context as any).dataSources = dataSources;
}
if (optionsObject.cacheControl !== false) {
if (
typeof optionsObject.cacheControl === 'boolean' &&
optionsObject.cacheControl === true
) {
// cacheControl: true means that the user needs the cache-control
// extensions. This means we are running the proxy, so we should not
// strip out the cache control extension and not add cache-control headers
cacheControl = {
stripFormattedExtensions: false,
calculateHttpHeaders: false,
defaultMaxAge: 0,
};
} else {
// Default behavior is to run default header calculation and return
// no cacheControl extensions
cacheControl = {
stripFormattedExtensions: true,
calculateHttpHeaders: true,
defaultMaxAge: 0,
...optionsObject.cacheControl,
};
}
}
let params: QueryOptions = {
schema: optionsObject.schema,
queryString,
@ -313,16 +363,22 @@ export async function runHttpQuery(
context,
rootValue: optionsObject.rootValue,
operationName: operationName,
logFunction: optionsObject.logFunction,
validationRules: optionsObject.validationRules,
formatError: optionsObject.formatError,
formatResponse: optionsObject.formatResponse,
fieldResolver: optionsObject.fieldResolver,
debug: optionsObject.debug,
tracing: optionsObject.tracing,
cacheControl: optionsObject.cacheControl,
cacheControl: cacheControl
? omit(cacheControl, [
'calculateHttpHeaders',
'stripFormattedExtensions',
])
: false,
request: request.request,
extensions: optionsObject.extensions,
persistedQueryHit,
persistedQueryRegister,
};
if (optionsObject.formatParams) {
@ -339,7 +395,7 @@ export async function runHttpQuery(
// Populate any HttpQueryError to our handler which should
// convert it to Http Error.
if (e.name === 'HttpQueryError') {
//async function wraps this in a Promise
// async function wraps this in a Promise
throw e;
}
@ -347,25 +403,73 @@ export async function runHttpQuery(
errors: formatApolloErrors([e], {
formatter: optionsObject.formatError,
debug: optionsObject.debug,
logFunction: optionsObject.logFunction,
}),
};
}
}) as Array<Promise<ExecutionResult>>;
}) as Array<Promise<ExecutionResult & { extensions?: Record<string, any> }>>;
const responses = await Promise.all(requests);
if (!isBatch) {
const gqlResponse = responses[0];
//This code is run on parse/validation errors and any other error that
//doesn't reach GraphQL execution
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') {
throw new HttpQueryError(400, prettyJSONStringify(gqlResponse), true, {
'Content-Type': 'application/json',
const responseInit: ApolloServerHttpResponse = {
headers: {
'Content-Type': 'application/json',
},
};
if (cacheControl) {
if (cacheControl.calculateHttpHeaders) {
const calculatedHeaders =
typeof cacheControl.calculateHttpHeaders === 'function'
? cacheControl.calculateHttpHeaders(responses)
: calculateCacheControlHeaders(responses);
responseInit.headers = {
...responseInit.headers,
...calculatedHeaders,
};
}
if (cacheControl.stripFormattedExtensions) {
responses.forEach(response => {
if (response.extensions) {
delete response.extensions.cacheControl;
if (Object.keys(response.extensions).length === 0) {
delete response.extensions;
}
}
});
}
return prettyJSONStringify(gqlResponse);
}
return prettyJSONStringify(responses);
if (!isBatch) {
const graphqlResponse = responses[0];
// This code is run on parse/validation errors and any other error that
// doesn't reach GraphQL execution
if (graphqlResponse.errors && typeof graphqlResponse.data === 'undefined') {
throwHttpGraphQLError(400, graphqlResponse.errors as any, optionsObject);
}
const stringified = prettyJSONStringify(graphqlResponse);
responseInit['Content-Length'] = Buffer.byteLength(
stringified,
'utf8',
).toString();
return {
graphqlResponse: stringified,
responseInit,
};
}
const stringified = prettyJSONStringify(responses);
responseInit['Content-Length'] = Buffer.byteLength(
stringified,
'utf8',
).toString();
return {
graphqlResponse: stringified,
responseInit,
};
}

View file

@ -1,7 +1,7 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai';
import { stub } from 'sinon';
import MockReq from 'mock-req';
import * as MockReq from 'mock-req';
import 'mocha';
import {
@ -14,7 +14,6 @@ import {
} from 'graphql';
import { runQuery } from './runQuery';
import { LogAction, LogStep } from './logging';
// Make the global Promise constructor Fiber-aware to simulate a Meteor
// environment.
@ -277,59 +276,6 @@ describe('runQuery', () => {
});
});
it('calls logFunction', () => {
const query = `
query Q1 {
testString
}`;
const logs = [];
const logFn = obj => logs.push(obj);
const expected = {
testString: 'it works',
};
return runQuery({
schema,
queryString: query,
operationName: 'Q1',
variables: { test: 123 },
logFunction: logFn,
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
expect(logs.length).to.equals(11);
expect(logs[0]).to.deep.equals({
action: LogAction.request,
step: LogStep.start,
});
expect(logs[1]).to.deep.equals({
action: LogAction.request,
step: LogStep.status,
key: 'query',
data: query,
});
expect(logs[2]).to.deep.equals({
action: LogAction.request,
step: LogStep.status,
key: 'variables',
data: { test: 123 },
});
expect(logs[3]).to.deep.equals({
action: LogAction.request,
step: LogStep.status,
key: 'operationName',
data: 'Q1',
});
expect(logs[10]).to.deep.equals({
action: LogAction.request,
step: LogStep.end,
key: 'response',
data: {
data: expected,
},
});
});
});
it('uses custom field resolver', async () => {
const query = `
query Q1 {

View file

@ -19,16 +19,17 @@ import {
GraphQLExtensionStack,
} from 'graphql-extensions';
import { TracingExtension } from 'apollo-tracing';
import { CacheControlExtension } from 'apollo-cache-control';
import {
CacheControlExtension,
CacheControlExtensionOptions,
} from 'apollo-cache-control';
import {
fromGraphQLError,
formatApolloErrors,
ValidationError,
SyntaxError,
} from './errors';
import { LogFunction, LogFunctionExtension } from './logging';
} from 'apollo-server-errors';
export interface GraphQLResponse {
data?: object;
@ -51,7 +52,6 @@ export interface QueryOptions {
context?: any;
variables?: { [key: string]: any };
operationName?: string;
logFunction?: LogFunction;
validationRules?: Array<(context: ValidationContext) => any>;
fieldResolver?: GraphQLFieldResolver<any, any>;
// WARNING: these extra validation rules are only applied to queries
@ -61,10 +61,11 @@ export interface QueryOptions {
formatResponse?: Function;
debug?: boolean;
tracing?: boolean;
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
cacheControl?: boolean | CacheControlExtensionOptions;
request: Pick<Request, 'url' | 'method' | 'headers'>;
extensions?: Array<() => GraphQLExtension>;
persistedQueryHit?: boolean;
persistedQueryRegister?: boolean;
}
function isQueryOperation(query: DocumentNode, operationName: string) {
@ -105,9 +106,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
} else if (options.cacheControl) {
extensions.push(new CacheControlExtension(options.cacheControl));
}
if (options.logFunction) {
extensions.push(new LogFunctionExtension(options.logFunction));
}
const extensionStack = new GraphQLExtensionStack(extensions);
@ -133,6 +131,8 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
parsedQuery: options.parsedQuery,
operationName: options.operationName,
variables: options.variables,
persistedQueryHit: options.persistedQueryHit,
persistedQueryRegister: options.persistedQueryRegister,
});
return Promise.resolve()
.then(() => {
@ -198,7 +198,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
),
{
formatter: options.formatError,
logFunction: options.logFunction,
debug,
},
);
@ -250,7 +249,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
if (result.errors) {
response.errors = formatApolloErrors([...result.errors], {
formatter: options.formatError,
logFunction: options.logFunction,
debug,
});
}

View file

@ -1,7 +1,7 @@
import { GraphQLSchema, DocumentNode } from 'graphql';
import { SchemaDirectiveVisitor, IResolvers, IMocks } from 'graphql-tools';
import { ConnectionContext } from 'subscriptions-transport-ws';
import WebSocket from 'ws';
import * as WebSocket from 'ws';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingOptions } from 'apollo-engine-reporting';
export { GraphQLExtension } from 'graphql-extensions';
@ -29,13 +29,14 @@ export interface SubscriptionServerOptions {
onDisconnect?: (websocket: WebSocket, context: ConnectionContext) => any;
}
// This configuration is shared between all integrations and should include
// fields that are not specific to a single integration
export interface Config
extends Pick<
GraphQLOptions<Context<any>>,
| 'formatError'
| 'debug'
| 'rootValue'
| 'logFunction'
| 'formatParams'
| 'validationRules'
| 'formatResponse'
@ -45,7 +46,7 @@ export interface Config
| 'dataSources'
| 'cache'
> {
typeDefs?: DocumentNode | [DocumentNode];
typeDefs?: DocumentNode | Array<DocumentNode>;
resolvers?: IResolvers;
schema?: GraphQLSchema;
schemaDirectives?: Record<string, typeof SchemaDirectiveVisitor>;
@ -56,6 +57,17 @@ export interface Config
extensions?: Array<() => GraphQLExtension>;
persistedQueries?: PersistedQueryOptions | false;
subscriptions?: Partial<SubscriptionServerOptions> | string | false;
//https://github.com/jaydenseric/apollo-upload-server#options
uploads?: boolean | FileUploadOptions;
}
export interface FileUploadOptions {
//Max allowed non-file multipart form field size in bytes; enough for your queries (default: 1 MB).
maxFieldSize?: number;
//Max allowed file size in bytes (default: Infinity).
maxFileSize?: number;
//Max allowed number of files (default: Infinity).
maxFiles?: number;
}
export interface MiddlewareOptions {

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-env",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {

View file

@ -0,0 +1,6 @@
*
!src/**/*
!dist/**/*
dist/**/*.test.*
!package.json
!README.md

View file

@ -0,0 +1,49 @@
{
"name": "apollo-server-errors",
"version": "2.0.0-rc.3",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-errors"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"scripts": {
"clean": "rm -rf lib",
"compile": "tsc",
"prepublish": "npm run clean && npm run compile"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": ">=6"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"devDependencies": {
"@types/jest": "^23.0.0",
"jest": "^23.1.0",
"ts-jest": "^22.4.6"
},
"jest": {
"testEnvironment": "node",
"transform": {
"^.+\\.(ts|js)$": "ts-jest"
},
"moduleFileExtensions": [
"ts",
"js",
"json"
],
"testRegex": "/__tests__/.*$",
"globals": {
"ts-jest": {
"skipBabel": true
}
}
}
}

View file

@ -1,5 +1,4 @@
import { GraphQLError } from 'graphql';
import { LogStep, LogAction, LogFunction } from './logging';
export class ApolloError extends Error implements GraphQLError {
public extensions: Record<string, any>;
@ -26,13 +25,13 @@ export class ApolloError extends Error implements GraphQLError {
});
}
//if no name provided, use the default. defineProperty ensures that it stays non-enumerable
// if no name provided, use the default. defineProperty ensures that it stays non-enumerable
if (!this.name) {
Object.defineProperty(this, 'name', { value: 'ApolloError' });
}
//extensions are flattened to be included in the root of GraphQLError's, so
//don't add properties to extensions
// extensions are flattened to be included in the root of GraphQLError's, so
// don't add properties to extensions
this.extensions = { code };
}
}
@ -82,9 +81,9 @@ function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
},
};
//ensure that extensions is not taken from the originalError
//graphql-js ensures that the originalError's extensions are hoisted
//https://github.com/graphql/graphql-js/blob/0bb47b2/src/error/GraphQLError.js#L138
// ensure that extensions is not taken from the originalError
// graphql-js ensures that the originalError's extensions are hoisted
// https://github.com/graphql/graphql-js/blob/0bb47b2/src/error/GraphQLError.js#L138
delete expanded.extensions.exception.extensions;
if (debug && !expanded.extensions.exception.stacktrace) {
expanded.extensions.exception.stacktrace =
@ -95,7 +94,7 @@ function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
}
if (Object.keys(expanded.extensions.exception).length === 0) {
//remove from printing an empty object
// remove from printing an empty object
delete expanded.extensions.exception;
}
@ -126,24 +125,24 @@ export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
? new options.errorClass(error.message)
: new ApolloError(error.message);
//copy enumerable keys
// copy enumerable keys
Object.keys(error).forEach(key => {
copy[key] = error[key];
});
//extensions are non enumerable, so copy them directly
// extensions are non enumerable, so copy them directly
copy.extensions = {
...copy.extensions,
...error.extensions,
};
//Fallback on default for code
// Fallback on default for code
if (!copy.extensions.code) {
copy.extensions.code = (options && options.code) || 'INTERNAL_SERVER_ERROR';
}
//copy the original error, while keeping all values non-enumerable, so they
//are not printed unless directly referenced
// copy the original error, while keeping all values non-enumerable, so they
// are not printed unless directly referenced
Object.defineProperty(copy, 'originalError', { value: {} });
Object.getOwnPropertyNames(error).forEach(key => {
Object.defineProperty(copy.originalError, key, { value: error[key] });
@ -216,16 +215,15 @@ export function formatApolloErrors(
errors: Array<Error>,
options?: {
formatter?: Function;
logFunction?: LogFunction;
debug?: boolean;
},
): Array<ApolloError> {
if (!options) {
return errors.map(error => enrichError(error));
}
const { formatter, debug, logFunction } = options;
const { formatter, debug } = options;
const flattenedErrors = [];
const flattenedErrors: Error[] = [];
errors.forEach(error => {
// Errors that occur in graphql-tools can contain an errors array that contains the errors thrown in a merged schema
// https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L104-L107
@ -258,18 +256,10 @@ export function formatApolloErrors(
try {
return formatter(error);
} catch (err) {
logFunction &&
logFunction({
action: LogAction.cleanup,
step: LogStep.status,
data: err,
key: 'error',
});
if (debug) {
return enrichError(err, debug);
} else {
//obscure error
// obscure error
const newError = fromGraphQLError(
new GraphQLError('Internal server error'),
);

View file

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"removeComments": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/__tests__/*", "**/__mocks__/*"]
}

View file

@ -8,15 +8,14 @@ description: Setting up Apollo Server with Express.js or Connect
This is the Express and Connect integration of GraphQL Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/). [Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md)
```sh
npm install apollo-server@beta apollo-server-express@beta
npm install apollo-server-express@rc
```
## Express
```js
const express = require('express');
const { registerServer } = require('apollo-server-express');
const { ApolloServer, gql } = require('apollo-server');
const { ApolloServer, gql } = require('apollo-server-express');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
@ -35,19 +34,19 @@ const resolvers = {
const server = new ApolloServer({ typeDefs, resolvers });
const app = express();
registerServer({ server, app });
server.applyMiddleware({ app });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
```
## Connect
```js
import connect from 'connect';
const { registerServer } = require('apollo-server-express');
const { ApolloServer, gql } = require('apollo-server');
const connect = require('connect');
const { ApolloServer, gql } = require('apollo-server-express');
const query = require('qs-middleware');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
@ -66,13 +65,18 @@ const resolvers = {
const server = new ApolloServer({ typeDefs, resolvers });
const app = connect();
registerServer({ server, app });
const path = '/graphql';
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
server.use(query());
server.applyMiddleware({ app, path });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
```
> Note; `qs-middleware` is only required if running outside of Meteor
## Principles
GraphQL Server is built with the following principles in mind:

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-express",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"description": "Production-ready Node.js GraphQL server for Express and Connect",
"main": "dist/index.js",
"scripts": {
@ -30,8 +30,11 @@
},
"dependencies": {
"@types/accepts": "^1.3.5",
"@types/body-parser": "1.17.0",
"@types/cors": "^2.8.4",
"@types/express": "4.16.0",
"accepts": "^1.3.5",
"apollo-server-core": "^2.0.0-rc.0",
"apollo-server-core": "^2.0.0-rc.3",
"apollo-upload-server": "^5.0.0",
"body-parser": "^1.18.3",
"cors": "^2.8.4",
@ -39,13 +42,10 @@
"type-is": "^1.6.16"
},
"devDependencies": {
"@types/body-parser": "1.17.0",
"@types/connect": "3.4.32",
"@types/cors": "^2.8.4",
"@types/express": "4.16.0",
"@types/multer": "1.3.6",
"apollo-datasource-rest": "^2.0.0-rc.0",
"apollo-server-integration-testsuite": "^2.0.0-rc.0",
"apollo-datasource-rest": "^2.0.0-rc.3",
"apollo-server-integration-testsuite": "^2.0.0-rc.3",
"connect": "3.6.6",
"express": "4.16.3",
"form-data": "^2.3.2",
@ -54,6 +54,9 @@
"node-fetch": "^2.1.2",
"qs-middleware": "^1.0.3"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"

View file

@ -1,27 +1,22 @@
import { expect } from 'chai';
import 'mocha';
import express from 'express';
import * as express from 'express';
import net from 'net';
import http from 'http';
import * as http from 'http';
import request from 'request';
import FormData from 'form-data';
import fs from 'fs';
import fetch from 'node-fetch';
import * as request from 'request';
import * as FormData from 'form-data';
import * as fs from 'fs';
import { createApolloFetch } from 'apollo-fetch';
import { ApolloServerBase, AuthenticationError } from 'apollo-server-core';
import { ApolloServer } from './ApolloServer';
import { gql, AuthenticationError, Config } from 'apollo-server-core';
import { ApolloServer, ServerRegistration } from './ApolloServer';
import {
testApolloServer,
createServerInfo,
} from 'apollo-server-integration-testsuite';
//to remove the circular dependency, we reference it directly
const gql = require('../../apollo-server/dist/index').gql;
const typeDefs = gql`
type Query {
hello: String
@ -34,9 +29,6 @@ const resolvers = {
},
};
const url = 'http://localhost:4000/graphql';
const uri = url;
describe('apollo-server-express', () => {
let server;
let httpServer;
@ -58,13 +50,27 @@ describe('apollo-server-express', () => {
});
describe('apollo-server-express', () => {
//to remove the circular dependency, we reference it directly
const ApolloServer = require('../../apollo-server/dist/index').ApolloServer;
let server: ApolloServerBase | any;
let server: ApolloServer;
let app: express.Application;
let httpServer: http.Server;
async function createServer(
serverOptions: Config,
options: Partial<ServerRegistration> = {},
) {
server = new ApolloServer(serverOptions);
app = express();
server.applyMiddleware({ ...options, app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
return createServerInfo(server, httpServer);
}
afterEach(async () => {
if (server) await server.stop();
if (httpServer) await httpServer.close();
@ -72,31 +78,16 @@ describe('apollo-server-express', () => {
describe('constructor', () => {
it('accepts typeDefs and resolvers', () => {
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
expect(() => server.applyMiddleware({ app })).not.to.throw;
});
it('accepts typeDefs and mocks', () => {
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
expect(() => server.applyMiddleware({ app })).not.to.throw;
return createServer({ typeDefs, resolvers });
});
});
describe('applyMiddleware', () => {
it('can be queried', async () => {
server = new ApolloServer({
const { url: uri } = await createServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({ app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
@ -118,19 +109,13 @@ describe('apollo-server-express', () => {
}
`;
server = new ApolloServer({
const { url: uri } = await createServer({
typeDefs,
resolvers,
introspection: false,
});
app = express();
server.applyMiddleware({ app, gui: true });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const apolloFetch = createApolloFetch({ uri: url });
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: INTROSPECTION_QUERY });
expect(result.errors.length).to.equal(1);
@ -141,7 +126,7 @@ describe('apollo-server-express', () => {
return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
url: uri,
method: 'GET',
headers: {
accept:
@ -165,17 +150,11 @@ describe('apollo-server-express', () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
server = new ApolloServer({
const { url } = await createServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({ app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
return new Promise<http.Server>((resolve, reject) => {
request(
{
@ -201,17 +180,15 @@ describe('apollo-server-express', () => {
});
it('accepts cors configuration', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({ app, cors: { origin: 'apollographql.com' } });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const { url: uri } = await createServer(
{
typeDefs,
resolvers,
},
{
cors: { origin: 'apollographql.com' },
},
);
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
@ -225,17 +202,15 @@ describe('apollo-server-express', () => {
});
it('accepts body parser configuration', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({ app, bodyParserConfig: { limit: 0 } });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const { url: uri } = await createServer(
{
typeDefs,
resolvers,
},
{
bodyParserConfig: { limit: 0 },
},
);
const apolloFetch = createApolloFetch({ uri });
@ -257,18 +232,10 @@ describe('apollo-server-express', () => {
});
it('creates a healthcheck endpoint', async () => {
server = new ApolloServer({
const { port } = await createServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({ app, bodyParserConfig: { limit: 0 } });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const { port } = httpServer.address() as net.AddressInfo;
return new Promise((resolve, reject) => {
request(
@ -290,23 +257,17 @@ describe('apollo-server-express', () => {
});
it('provides a callback for the healthcheck', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({
app,
onHealthCheck: async () => {
throw Error("can't connect to DB");
const { port } = await createServer(
{
typeDefs,
resolvers,
},
});
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const { port } = httpServer.address() as net.AddressInfo;
{
onHealthCheck: async () => {
throw Error("can't connect to DB");
},
},
);
return new Promise((resolve, reject) => {
request(
@ -328,20 +289,15 @@ describe('apollo-server-express', () => {
});
it('can disable the healthCheck', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
server.applyMiddleware({
app,
disableHealthCheck: true,
});
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const { port } = httpServer.address() as net.AddressInfo;
const { port } = await createServer(
{
typeDefs,
resolvers,
},
{
disableHealthCheck: true,
},
);
return new Promise((resolve, reject) => {
request(
@ -362,8 +318,13 @@ describe('apollo-server-express', () => {
});
});
describe('file uploads', () => {
xit('enabled uploads', async () => {
server = new ApolloServer({
it('enabled uploads', async () => {
// XXX This is currently a failing test for node 10
const NODE_VERSION = process.version.split('.');
const NODE_MAJOR_VERSION = parseInt(NODE_VERSION[0].replace(/^v/, ''));
if (NODE_MAJOR_VERSION === 10) return;
const { port } = await createServer({
typeDefs: gql`
type File {
filename: String!
@ -391,15 +352,6 @@ describe('apollo-server-express', () => {
},
},
});
app = express();
server.applyMiddleware({
app,
});
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const { port } = httpServer.address() as net.AddressInfo;
const body = new FormData();
@ -427,9 +379,10 @@ describe('apollo-server-express', () => {
try {
const resolved = await fetch(`http://localhost:${port}/graphql`, {
method: 'POST',
body,
body: body as any,
});
const response = await resolved.json();
const text = await resolved.text();
const response = JSON.parse(text);
expect(response.data.singleUpload).to.deep.equal({
filename: 'package.json',
@ -460,7 +413,7 @@ describe('apollo-server-express', () => {
},
},
};
server = new ApolloServer({
const { url: uri } = await createServer({
typeDefs,
resolvers,
context: () => {
@ -468,12 +421,6 @@ describe('apollo-server-express', () => {
},
});
app = express();
server.applyMiddleware({ app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
@ -493,7 +440,7 @@ describe('apollo-server-express', () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
server = new ApolloServer({
const { url: uri } = await createServer({
typeDefs: gql`
type Query {
error: String
@ -508,12 +455,6 @@ describe('apollo-server-express', () => {
},
});
app = express();
server.applyMiddleware({ app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
@ -533,7 +474,7 @@ describe('apollo-server-express', () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
server = new ApolloServer({
const { url: uri } = await createServer({
typeDefs: gql`
type Query {
error: String
@ -548,12 +489,6 @@ describe('apollo-server-express', () => {
},
});
app = express();
server.applyMiddleware({ app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
@ -572,7 +507,7 @@ describe('apollo-server-express', () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
server = new ApolloServer({
const { url: uri } = await createServer({
typeDefs: gql`
type Query {
error: String!
@ -587,12 +522,6 @@ describe('apollo-server-express', () => {
},
});
app = express();
server.applyMiddleware({ app });
httpServer = await new Promise<http.Server>(resolve => {
const l = app.listen({ port: 4000 }, () => resolve(l));
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
@ -607,4 +536,217 @@ describe('apollo-server-express', () => {
});
});
});
describe('extensions', () => {
const books = [
{
title: 'H',
author: 'J',
},
];
const typeDefs = gql`
type Book {
title: String
author: String
}
type Cook @cacheControl(maxAge: 200) {
title: String
author: String
}
type Pook @cacheControl(maxAge: 200) {
title: String
books: [Book] @cacheControl(maxAge: 20, scope: PRIVATE)
}
type Query {
books: [Book]
cooks: [Cook]
pooks: [Pook]
}
`;
const resolvers = {
Query: {
books: () => books,
cooks: () => books,
pooks: () => [{ title: 'pook', books }],
},
};
describe('Cache Control Headers', () => {
it('applies cacheControl Headers and strips out extension', async () => {
const { url: uri } = await createServer({ typeDefs, resolvers });
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).to.equal(
'max-age=200, public',
);
next();
},
);
const result = await apolloFetch({
query: `{ cooks { title author } }`,
});
expect(result.data).to.deep.equal({ cooks: books });
expect(result.extensions).not.to.exist;
});
it('contains no cacheControl Headers and keeps extension with engine proxy', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
cacheControl: true,
});
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).not.to.exist;
next();
},
);
const result = await apolloFetch({
query: `{ cooks { title author } }`,
});
expect(result.data).to.deep.equal({ cooks: books });
expect(result.extensions).to.exist;
expect(result.extensions.cacheControl).to.exist;
});
it('contains no cacheControl Headers when uncachable', async () => {
const { url: uri } = await createServer({ typeDefs, resolvers });
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).not.to.exist;
next();
},
);
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).to.deep.equal({ books });
expect(result.extensions).not.to.exist;
});
it('contains private cacheControl Headers when scoped', async () => {
const { url: uri } = await createServer({ typeDefs, resolvers });
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).to.equal(
'max-age=20, private',
);
next();
},
);
const result = await apolloFetch({
query: `{ pooks { title books { title author } } }`,
});
expect(result.data).to.deep.equal({
pooks: [{ title: 'pook', books }],
});
expect(result.extensions).not.to.exist;
});
it('runs when cache-control is false', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
cacheControl: false,
});
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).null;
next();
},
);
const result = await apolloFetch({
query: `{ pooks { title books { title author } } }`,
});
expect(result.data).to.deep.equal({
pooks: [{ title: 'pook', books }],
});
expect(result.extensions).not.to.exist;
});
});
describe('Tracing', () => {
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`;
const resolvers = {
Query: {
books: () => books,
},
};
it('applies tracing extension', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
tracing: true,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).to.deep.equal({ books });
expect(result.extensions).to.exist;
expect(result.extensions.tracing).to.exist;
});
it('applies tracing extension with cache control enabled', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
tracing: true,
cacheControl: true,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).to.deep.equal({ books });
expect(result.extensions).to.exist;
expect(result.extensions.tracing).to.exist;
});
xit('applies tracing extension with engine enabled', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
tracing: true,
engine: {
apiKey: 'fake',
maxAttempts: 0,
endpointUrl: 'l',
reportErrorFunction: () => {},
},
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).to.deep.equal({ books });
expect(result.extensions).to.exist;
expect(result.extensions.tracing).to.exist;
});
});
});
});

View file

@ -1,21 +1,18 @@
import express from 'express';
import corsMiddleware from 'cors';
import * as express from 'express';
import * as corsMiddleware from 'cors';
import { json, OptionsJson } from 'body-parser';
import playgroundMiddleware from 'graphql-playground-middleware-express';
import { MiddlewareOptions as PlaygroundMiddlewareOptions } from 'graphql-playground-html';
import { ApolloServerBase, formatApolloErrors } from 'apollo-server-core';
import accepts from 'accepts';
import typeis from 'type-is';
import * as accepts from 'accepts';
import * as typeis from 'type-is';
import { graphqlExpress } from './expressApollo';
import {
processRequest as processFileUploads,
GraphQLUpload,
} from 'apollo-upload-server';
import { processRequest as processFileUploads } from 'apollo-upload-server';
export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';
import { GraphQLOptions, gql, makeExecutableSchema } from 'apollo-server-core';
import { GraphQLOptions, FileUploadOptions } from 'apollo-server-core';
export interface ServerRegistration {
// Note: You can also pass a connect.Server here. If we changed this field to
@ -31,12 +28,10 @@ export interface ServerRegistration {
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
gui?: boolean | PlaygroundMiddlewareOptions;
//https://github.com/jaydenseric/apollo-upload-server#options
uploads?: boolean | Record<string, any>;
}
const fileUploadMiddleware = (
uploadsConfig: Record<string, any>,
uploadsConfig: FileUploadOptions,
server: ApolloServerBase,
) => (
req: express.Request,
@ -57,7 +52,6 @@ const fileUploadMiddleware = (
formatApolloErrors([error], {
formatter: server.requestOptions.formatError,
debug: server.requestOptions.debug,
logFunction: server.requestOptions.logFunction,
}),
);
});
@ -67,9 +61,9 @@ const fileUploadMiddleware = (
};
export class ApolloServer extends ApolloServerBase {
//This translates the arguments from the middleware into graphQL options It
//provides typings for the integration specific behavior, ideally this would
//be propagated with a generic to the super class
// This translates the arguments from the middleware into graphQL options It
// provides typings for the integration specific behavior, ideally this would
// be propagated with a generic to the super class
async createGraphQLServerOptions(
req: express.Request,
res: express.Response,
@ -81,6 +75,10 @@ export class ApolloServer extends ApolloServerBase {
return true;
}
protected supportsUploads(): boolean {
return true;
}
public applyMiddleware({
app,
path,
@ -89,14 +87,13 @@ export class ApolloServer extends ApolloServerBase {
disableHealthCheck,
gui,
onHealthCheck,
uploads,
}: ServerRegistration) {
if (!path) path = '/graphql';
if (!disableHealthCheck) {
//uses same path as engine proxy, but is generally useful.
// uses same path as engine proxy, but is generally useful.
app.use('/.well-known/apollo/server-health', (req, res) => {
//Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');
if (onHealthCheck) {
@ -114,20 +111,8 @@ export class ApolloServer extends ApolloServerBase {
}
let uploadsMiddleware;
if (uploads !== false) {
this.enhanceSchema(
makeExecutableSchema({
typeDefs: gql`
scalar Upload
`,
resolvers: { Upload: GraphQLUpload },
}),
);
uploadsMiddleware = fileUploadMiddleware(
typeof uploads !== 'boolean' ? uploads : {},
this,
);
if (this.uploadsConfig) {
uploadsMiddleware = fileUploadMiddleware(this.uploadsConfig, this);
}
// XXX multiple paths?
@ -160,7 +145,7 @@ export class ApolloServer extends ApolloServerBase {
app.use(path, (req, res, next) => {
if (guiEnabled && req.method === 'GET') {
//perform more expensive content-type check only if necessary
// perform more expensive content-type check only if necessary
const accept = accepts(req);
const types = accept.types() as string[];
const prefersHTML =

View file

@ -22,9 +22,9 @@ import { graphqlExpress } from './expressApollo';
*/
import { expect } from 'chai';
import zlib from 'zlib';
import multer from 'multer';
import bodyParser from 'body-parser';
import * as zlib from 'zlib';
import * as multer from 'multer';
import * as bodyParser from 'body-parser';
const request = require('supertest');
const express4 = require('express'); // modern
import {

View file

@ -1,5 +1,5 @@
import connect from 'connect';
import query from 'qs-middleware';
import * as connect from 'connect';
import * as query from 'qs-middleware';
import { ApolloServer } from './ApolloServer';
import { Config } from 'apollo-server-core';
import 'mocha';

View file

@ -1,8 +1,8 @@
import { expect } from 'chai';
import 'mocha';
import express from 'express';
import * as express from 'express';
import http from 'http';
import * as http from 'http';
import { RESTDataSource } from 'apollo-datasource-rest';
@ -25,7 +25,7 @@ export class IdAPI extends RESTDataSource {
}
}
//to remove the circular dependency, we reference it directly
// to remove the circular dependency, we reference it directly
const gql = require('../../apollo-server/dist/index').gql;
const typeDefs = gql`

View file

@ -1,4 +1,4 @@
import express from 'express';
import * as express from 'express';
import { ApolloServer } from './ApolloServer';
import testSuite, {
schema as Schema,

View file

@ -1,4 +1,4 @@
import express from 'express';
import * as express from 'express';
import {
GraphQLOptions,
HttpQueryError,
@ -46,13 +46,11 @@ export function graphqlExpress(
query: req.method === 'POST' ? req.body : req.query,
request: convertNodeHttpToRequest(req),
}).then(
gqlResponse => {
res.setHeader('Content-Type', 'application/json');
res.setHeader(
'Content-Length',
Buffer.byteLength(gqlResponse, 'utf8').toString(),
({ graphqlResponse, responseInit }) => {
Object.keys(responseInit.headers).forEach(key =>
res.setHeader(key, responseInit.headers[key]),
);
res.write(gqlResponse);
res.write(graphqlResponse);
res.end();
},
(error: HttpQueryError) => {

View file

@ -8,6 +8,7 @@ export {
ValidationError,
AuthenticationError,
ForbiddenError,
UserInputError,
} from 'apollo-server-core';
// ApolloServer integration

View file

@ -8,74 +8,31 @@ description: Setting up Apollo Server with Hapi
This is the Hapi integration of Apollo Server. Apollo Server is a community-maintained open-source Apollo Server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/). [Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md)
```sh
npm install apollo-server-hapi@beta
npm install apollo-server-hapi@rc
```
## Usage
After constructing Apollo server, a hapi server can be enabled with a call to `registerServer`. Ensure that `autoListen` is set to false in the `Hapi.server` constructor.
The code below requires Hapi 17 or higher.
```js
const { registerServer, ApolloServer, gql } = require('apollo-server-hapi');
const HOST = 'localhost';
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hello',
},
}
async function StartServer() {
const server = new ApolloServer({ typeDefs, resolvers });
await registerServer({
server,
//Hapi Server constructor options
options: {
host: HOST,
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
}
StartServer().catch(error => console.log(error));
```
For more advanced use cases or migrating from 1.x, a Hapi server can be constructed and passed into `registerServer`.
```js
const { ApolloServer, gql, registerServer } = require('apollo-server-hapi');
const { ApolloServer, gql } = require('apollo-server-hapi');
const Hapi = require('hapi');
async function StartServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Hapi.server({
//autoListen must be set to false, since Apollo Server will setup the listener
autoListen: false,
host: HOST,
port: 4000
});
await registerServer({
server,
await server.applyMiddleware({
app,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
await server.installSubscriptionHandlers(app.listener);
await app.start();
}
StartServer().catch(error => console.log(error));

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-hapi",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"description": "Production-ready Node.js GraphQL server for Hapi",
"main": "dist/index.js",
"scripts": {
@ -29,16 +29,19 @@
},
"dependencies": {
"accept": "^3.0.2",
"apollo-server-core": "^2.0.0-rc.0",
"apollo-server-core": "^2.0.0-rc.3",
"apollo-upload-server": "^5.0.0",
"boom": "^7.1.0",
"graphql-playground-html": "^1.6.0"
},
"devDependencies": {
"@types/hapi": "^17.0.12",
"apollo-server-integration-testsuite": "^2.0.0-rc.0",
"apollo-server-integration-testsuite": "^2.0.0-rc.3",
"hapi": "17.4.0"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"

View file

@ -1,20 +1,31 @@
import { expect } from 'chai';
import 'mocha';
import { Server } from 'hapi';
import {
testApolloServer,
createServerInfo,
} from 'apollo-server-integration-testsuite';
import http = require('http');
import request = require('request');
import FormData = require('form-data');
import fs = require('fs');
import { createApolloFetch } from 'apollo-fetch';
import { gql, AuthenticationError } from 'apollo-server-core';
import { ApolloServer } from './ApolloServer';
describe('apollo-server-hapi', () => {
let server;
let app;
let httpServer;
let server: ApolloServer;
let app: Server;
let httpServer: http.Server;
testApolloServer(
async options => {
server = new ApolloServer(options);
app = new Server({ host: 'localhost', port: 4000 });
server.applyMiddleware({ app });
await server.applyMiddleware({ app });
await app.start();
const httpServer = app.listener;
return createServerInfo(server, httpServer);
@ -25,4 +36,556 @@ describe('apollo-server-hapi', () => {
if (httpServer && httpServer.listening) await httpServer.close();
},
);
//Non-integration tests
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hi',
},
};
afterEach(async () => {
if (server) await server.stop();
if (httpServer) await httpServer.close();
});
describe('constructor', () => {
it('accepts typeDefs and resolvers', async () => {
const app = new Server();
const server = new ApolloServer({ typeDefs, resolvers });
return server.applyMiddleware({ app });
});
});
describe('applyMiddleware', () => {
it('can be queried', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({ app });
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.data).to.deep.equal({ hello: 'hi' });
expect(result.errors, 'errors should exist').not.to.exist;
});
// XXX Unclear why this would be something somebody would want (vs enabling
// introspection without graphql-playground, which seems reasonable, eg you
// have your own graphql-playground setup with a custom link)
it('can enable gui separately from introspection during production', async () => {
const INTROSPECTION_QUERY = `
{
__schema {
directives {
name
}
}
}
`;
server = new ApolloServer({
typeDefs,
resolvers,
introspection: false,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({ app });
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const url = uri;
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: INTROSPECTION_QUERY });
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal(
'GRAPHQL_VALIDATION_FAILED',
);
return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
},
},
(error, response, body) => {
if (error) {
reject(error);
} else {
expect(body).to.contain('GraphQLPlayground');
expect(response.statusCode).to.equal(200);
resolve();
}
},
);
});
});
it('renders GraphQL playground when browser requests', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
server = new ApolloServer({
typeDefs,
resolvers,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({ app });
await app.start();
httpServer = app.listener;
const url = app.info.uri + '/graphql';
return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
},
},
(error, response, body) => {
process.env.NODE_ENV = nodeEnv;
if (error) {
reject(error);
} else {
expect(body).to.contain('GraphQLPlayground');
expect(response.statusCode).to.equal(200);
resolve();
}
},
);
});
});
// XXX currently this test fails. Unsure why, since the otpins are passed
// correctly to the server.route call in graphqlHapi
xit('accepts cors configuration', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
cors: { origin: ['apollographql.com'] },
});
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(
response.response.headers.get('access-control-allow-origin'),
).to.equal('apollographql.com');
next();
},
);
await apolloFetch({ query: '{hello}' });
});
describe('healthchecks', () => {
afterEach(async () => {
await server.stop();
});
it('creates a healthcheck endpoint', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({ app });
await app.start();
httpServer = app.listener;
const { port } = app.info;
return new Promise((resolve, reject) => {
request(
{
url: `http://localhost:${port}/.well-known/apollo/server-health`,
method: 'GET',
},
(error, response, body) => {
if (error) {
reject(error);
} else {
expect(body).to.equal(JSON.stringify({ status: 'pass' }));
expect(response.statusCode).to.equal(200);
resolve();
}
},
);
});
});
it('provides a callback for the healthcheck', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
onHealthCheck: async () => {
throw Error("can't connect to DB");
},
});
await app.start();
httpServer = app.listener;
const { port } = app.info;
return new Promise((resolve, reject) => {
request(
{
url: `http://localhost:${port}/.well-known/apollo/server-health`,
method: 'GET',
},
(error, response, body) => {
if (error) {
reject(error);
} else {
expect(body).to.equal(JSON.stringify({ status: 'fail' }));
expect(response.statusCode).to.equal(503);
resolve();
}
},
);
});
});
it('can disable the healthCheck', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
disableHealthCheck: true,
});
await app.start();
httpServer = app.listener;
const { port } = app.info;
return new Promise((resolve, reject) => {
request(
{
url: `http://localhost:${port}/.well-known/apollo/server-health`,
method: 'GET',
},
(error, response) => {
if (error) {
reject(error);
} else {
expect(response.statusCode).to.equal(404);
resolve();
}
},
);
});
});
});
describe('file uploads', () => {
xit('enabled uploads', async () => {
server = new ApolloServer({
typeDefs: gql`
type File {
filename: String!
mimetype: String!
encoding: String!
}
type Query {
uploads: [File]
}
type Mutation {
singleUpload(file: Upload!): File!
}
`,
resolvers: {
Query: {
uploads: () => {},
},
Mutation: {
singleUpload: async (_, args) => {
expect((await args.file).stream).to.exist;
return args.file;
},
},
},
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
disableHealthCheck: true,
});
await app.start();
httpServer = app.listener;
const { port } = app.info;
const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: `
mutation($file: Upload!) {
singleUpload(file: $file) {
filename
encoding
mimetype
}
}
`,
variables: {
file: null,
},
}),
);
body.append('map', JSON.stringify({ 1: ['variables.file'] }));
body.append('1', fs.createReadStream('package.json'));
try {
const resolved = await fetch(`http://localhost:${port}/graphql`, {
method: 'POST',
body: body as any,
});
const response = await resolved.json();
expect(response.data.singleUpload).to.deep.equal({
filename: 'package.json',
encoding: '7bit',
mimetype: 'application/json',
});
} catch (error) {
// This error began appearing randomly and seems to be a dev dependency bug.
// https://github.com/jaydenseric/apollo-upload-server/blob/18ecdbc7a1f8b69ad51b4affbd986400033303d4/test.js#L39-L42
if (error.code !== 'EPIPE') throw error;
}
});
});
describe('errors', () => {
it('returns thrown context error as a valid graphql result', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => {
throw Error('never get here');
},
},
};
server = new ApolloServer({
typeDefs,
resolvers,
context: () => {
throw new AuthenticationError('valid result');
},
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
disableHealthCheck: true,
});
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.errors.length).to.equal(1);
expect(result.data).not.to.exist;
const e = result.errors[0];
expect(e.message).to.contain('valid result');
expect(e.extensions).to.exist;
expect(e.extensions.code).to.equal('UNAUTHENTICATED');
expect(e.extensions.exception.stacktrace).to.exist;
process.env.NODE_ENV = nodeEnv;
});
it('propogates error codes in dev mode', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
server = new ApolloServer({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
disableHealthCheck: true,
});
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
expect(result.data).to.exist;
expect(result.data).to.deep.equal({ error: null });
expect(result.errors, 'errors should exist').to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).to.exist;
expect(result.errors[0].extensions.exception.stacktrace).to.exist;
process.env.NODE_ENV = nodeEnv;
});
it('propogates error codes in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
server = new ApolloServer({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
disableHealthCheck: true,
});
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
expect(result.data).to.exist;
expect(result.data).to.deep.equal({ error: null });
expect(result.errors, 'errors should exist').to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).not.to.exist;
process.env.NODE_ENV = nodeEnv;
});
it('propogates error codes with null response in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
server = new ApolloServer({
typeDefs: gql`
type Query {
error: String!
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
app = new Server({ port: 4000 });
await server.applyMiddleware({
app,
disableHealthCheck: true,
});
await app.start();
httpServer = app.listener;
const uri = app.info.uri + '/graphql';
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
expect(result.data).null;
expect(result.errors, 'errors should exist').to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).not.to.exist;
process.env.NODE_ENV = nodeEnv;
});
});
});
});

View file

@ -1,21 +1,18 @@
import hapi from 'hapi';
import * as hapi from 'hapi';
import { ApolloServerBase } from 'apollo-server-core';
import { parseAll } from 'accept';
import {
renderPlaygroundPage,
MiddlewareOptions as PlaygroundMiddlewareOptions,
} from 'graphql-playground-html';
import {
processRequest as processFileUploads,
GraphQLUpload,
} from 'apollo-upload-server';
import { processRequest as processFileUploads } from 'apollo-upload-server';
import { graphqlHapi } from './hapiApollo';
export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';
import { GraphQLOptions, gql, makeExecutableSchema } from 'apollo-server-core';
import { GraphQLOptions, FileUploadOptions } from 'apollo-server-core';
function handleFileUploads(uploadsConfig: Record<string, any>) {
function handleFileUploads(uploadsConfig: FileUploadOptions) {
return async (request: hapi.Request) => {
if (request.mime === 'multipart/form-data') {
Object.defineProperty(request, 'payload', {
@ -27,9 +24,9 @@ function handleFileUploads(uploadsConfig: Record<string, any>) {
}
export class ApolloServer extends ApolloServerBase {
//This translates the arguments from the middleware into graphQL options It
//provides typings for the integration specific behavior, ideally this would
//be propagated with a generic to the super class
// This translates the arguments from the middleware into graphQL options It
// provides typings for the integration specific behavior, ideally this would
// be propagated with a generic to the super class
async createGraphQLServerOptions(
request: hapi.Request,
h: hapi.ResponseToolkit,
@ -41,6 +38,10 @@ export class ApolloServer extends ApolloServerBase {
return true;
}
protected supportsUploads(): boolean {
return true;
}
public async applyMiddleware({
app,
cors,
@ -48,21 +49,9 @@ export class ApolloServer extends ApolloServerBase {
disableHealthCheck,
gui,
onHealthCheck,
uploads,
}: ServerRegistration) {
if (!path) path = '/graphql';
if (uploads !== false) {
this.enhanceSchema(
makeExecutableSchema({
typeDefs: gql`
scalar Upload
`,
resolvers: { Upload: GraphQLUpload },
}),
);
}
await app.ext({
type: 'onRequest',
method: async function(request, h) {
@ -70,10 +59,8 @@ export class ApolloServer extends ApolloServerBase {
return h.continue;
}
if (uploads !== false) {
await handleFileUploads(typeof uploads !== 'boolean' ? uploads : {})(
request,
);
if (this.uploadsConfig) {
await handleFileUploads(this.uploadsConfig)(request);
}
// Note: if you enable a gui in production and expect to be able to see your
@ -85,7 +72,7 @@ export class ApolloServer extends ApolloServerBase {
// enableGUI takes precedence over the server tools setting
if (guiEnabled && request.method === 'get') {
//perform more expensive content-type check only if necessary
// perform more expensive content-type check only if necessary
const accept = parseAll(request.headers);
const types = accept.mediaTypes as string[];
const prefersHTML =
@ -108,7 +95,7 @@ export class ApolloServer extends ApolloServerBase {
}
}
return h.continue;
},
}.bind(this),
});
if (!disableHealthCheck) {
@ -116,7 +103,7 @@ export class ApolloServer extends ApolloServerBase {
method: '*',
path: '/.well-known/apollo/server-health',
options: {
cors: typeof cors === 'boolean' ? cors : true,
cors: cors !== undefined ? cors : true,
},
handler: async function(request, h) {
if (onHealthCheck) {
@ -142,7 +129,7 @@ export class ApolloServer extends ApolloServerBase {
path: path,
graphqlOptions: this.createGraphQLServerOptions.bind(this),
route: {
cors: typeof cors === 'boolean' ? cors : true,
cors: cors !== undefined ? cors : true,
},
},
});
@ -154,7 +141,7 @@ export class ApolloServer extends ApolloServerBase {
export interface ServerRegistration {
app?: hapi.Server;
path?: string;
cors?: boolean;
cors?: boolean | hapi.RouteOptionsCors;
onHealthCheck?: (request: hapi.Request) => Promise<any>;
disableHealthCheck?: boolean;
gui?: boolean | PlaygroundMiddlewareOptions;

View file

@ -1,4 +1,4 @@
import hapi from 'hapi';
import * as hapi from 'hapi';
import { ApolloServer } from './ApolloServer';
import { Config } from 'apollo-server-core';
import 'mocha';

View file

@ -1,5 +1,5 @@
import Boom from 'boom';
import { Server, Request } from 'hapi';
import * as Boom from 'boom';
import { Server, Request, RouteOptions } from 'hapi';
import {
GraphQLOptions,
runHttpQuery,
@ -23,7 +23,7 @@ export interface HapiOptionsFunction {
export interface HapiPluginOptions {
path: string;
vhost?: string;
route?: any;
route?: RouteOptions;
graphqlOptions: GraphQLOptions | HapiOptionsFunction;
}
@ -33,7 +33,6 @@ const graphqlHapi: IPlugin = {
if (!options || !options.graphqlOptions) {
throw new Error('Apollo Server requires options.');
}
server.route({
method: ['GET', 'POST'],
path: options.path || '/graphql',
@ -41,19 +40,24 @@ const graphqlHapi: IPlugin = {
options: options.route || {},
handler: async (request, h) => {
try {
const gqlResponse = await runHttpQuery([request], {
method: request.method.toUpperCase(),
options: options.graphqlOptions,
query:
request.method === 'post'
? //TODO type payload as string or Record
(request.payload as any)
: request.query,
request: convertNodeHttpToRequest(request.raw.req),
});
const { graphqlResponse, responseInit } = await runHttpQuery(
[request],
{
method: request.method.toUpperCase(),
options: options.graphqlOptions,
query:
request.method === 'post'
? // TODO type payload as string or Record
(request.payload as any)
: request.query,
request: convertNodeHttpToRequest(request.raw.req),
},
);
const response = h.response(gqlResponse);
response.type('application/json');
const response = h.response(graphqlResponse);
Object.keys(responseInit.headers).forEach(key =>
response.header(key, responseInit.headers[key]),
);
return response;
} catch (error) {
if ('HttpQueryError' !== error.name) {

View file

@ -7,6 +7,7 @@ export {
ValidationError,
AuthenticationError,
ForbiddenError,
UserInputError,
} from 'apollo-server-core';
// ApolloServer integration

View file

@ -1,7 +1,7 @@
{
"name": "apollo-server-integration-testsuite",
"private": true,
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"description": "Apollo Server Integrations testsuite",
"main": "dist/index.js",
"scripts": {
@ -23,7 +23,7 @@
"node": ">=6"
},
"dependencies": {
"apollo-server-core": "^2.0.0-rc.0"
"apollo-server-core": "^2.0.0-rc.3"
},
"typings": "dist/index.d.ts",
"typescript": {

View file

@ -1,8 +1,8 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai';
import { stub } from 'sinon';
import http from 'http';
import net from 'net';
import * as http from 'http';
import * as net from 'net';
import 'mocha';
import { sha256 } from 'js-sha256';
@ -17,7 +17,7 @@ import {
import { PubSub } from 'graphql-subscriptions';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import WebSocket from 'ws';
import * as WebSocket from 'ws';
import { execute } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
@ -610,12 +610,12 @@ export function testApolloServer<AS extends ApolloServerBase>(
},
});
//Unfortunately the error connection is not propagated to the
//observable. What should happen is we provide a default onError
//function that notifies the returned observable and can cursomize
//the behavior with an option in the client constructor. If you're
//available to make a PR to the following please do!
//https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts
// Unfortunately the error connection is not propagated to the
// observable. What should happen is we provide a default onError
// function that notifies the returned observable and can cursomize
// the behavior with an option in the client constructor. If you're
// available to make a PR to the following please do!
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts
client.onError((_: Error) => {
done();
});
@ -774,9 +774,9 @@ export function testApolloServer<AS extends ApolloServerBase>(
expect(result.errors).not.to.exist;
});
//Apollo Fetch's result depends on the server implementation, if the
//statusText of the error is unparsable, then we'll fall into the catch,
//such as with express. If it is parsable, then we'll use the afterware
// Apollo Fetch's result depends on the server implementation, if the
// statusText of the error is unparsable, then we'll fall into the catch,
// such as with express. If it is parsable, then we'll use the afterware
it('returns error when hash does not match', async () => {
const apolloFetch = createApolloFetch({ uri }).useAfter((res, next) => {
expect(res.response.status).to.equal(400);

View file

@ -2,7 +2,7 @@ import { expect } from 'chai';
import { stub } from 'sinon';
import 'mocha';
//persisted query tests
// persisted query tests
import { sha256 } from 'js-sha256';
import { VERSION } from 'apollo-link-persisted-queries';
@ -17,8 +17,7 @@ import {
BREAK,
} from 'graphql';
// tslint:disable-next-line
const request = require('supertest');
import request = require('supertest');
import { GraphQLOptions, Config } from 'apollo-server-core';
import gql from 'graphql-tag';
@ -326,7 +325,14 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
it('can handle a basic request with cacheControl and defaultMaxAge', async () => {
app = await createApp({
graphqlOptions: { schema, cacheControl: { defaultMaxAge: 5 } },
graphqlOptions: {
schema,
cacheControl: {
defaultMaxAge: 5,
stripFormattedExtensions: false,
calculateCacheControlHeaders: false,
},
},
});
const expected = {
testPerson: { firstName: 'Jane' },
@ -936,6 +942,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
describe('Persisted Queries', () => {
const query = '{testString}';
const query2 = '{ testString }';
const hash = sha256
.create()
@ -948,6 +955,16 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
},
};
const extensions2 = {
persistedQuery: {
version: VERSION,
sha256Hash: sha256
.create()
.update(query2)
.hex(),
},
};
beforeEach(async () => {
const map = new Map<string, string>();
const cache = {
@ -999,6 +1016,51 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
expect(result.body.errors).not.to.exist;
});
it('returns with batched persisted queries', async () => {
const errors = await request(app)
.post('/graphql')
.send([
{
extensions,
},
{
extensions: extensions2,
},
]);
expect(errors.body[0].data).to.not.exist;
expect(errors.body[1].data).to.not.exist;
expect(errors.body[0].errors[0].message).to.equal(
'PersistedQueryNotFound',
);
expect(errors.body[0].errors[0].extensions.code).to.equal(
'PERSISTED_QUERY_NOT_FOUND',
);
expect(errors.body[1].errors[0].message).to.equal(
'PersistedQueryNotFound',
);
expect(errors.body[1].errors[0].extensions.code).to.equal(
'PERSISTED_QUERY_NOT_FOUND',
);
const result = await request(app)
.post('/graphql')
.send([
{
extensions,
query,
},
{
extensions: extensions2,
query: query2,
},
]);
expect(result.body[0].data).to.deep.equal({ testString: 'it works' });
expect(result.body[0].data).to.deep.equal({ testString: 'it works' });
expect(result.body.errors).not.to.exist;
});
it('returns result on the persisted query', async () => {
await request(app)
.post('/graphql')

View file

@ -0,0 +1,6 @@
*
!src/**/*
!dist/**/*
dist/**/*.test.*
!package.json
!README.md

View file

@ -0,0 +1,205 @@
---
title: Lambda
description: Setting up Apollo Server with AWS Lambda
---
[![npm version](https://badge.fury.io/js/apollo-server-lambda.svg)](https://badge.fury.io/js/apollo-server-lambda) [![Build Status](https://circleci.com/gh/apollographql/apollo-server.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-server) [![Coverage Status](https://coveralls.io/repos/github/apollographql/apollo-server/badge.svg?branch=master)](https://coveralls.io/github/apollographql/apollo-server?branch=master) [![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://www.apollographql.com/#slack)
This is the AWS Lambda integration of GraphQL Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/v2). [Read the CHANGELOG](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md).
```sh
npm install apollo-server-lambda@rc
```
## Deploying with AWS Serverless Application Model (SAM)
To deploy the AWS Lambda function we must create a Cloudformation Template and a S3 bucket to store the artifact (zip of source code) and template. We will use the [AWS Command Line Interface](https://aws.amazon.com/cli/).
#### 1. Write the API handlers
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
exports.graphqlHandler = server.createHandler();
```
#### 2. Create an S3 bucket
The bucket name must be universally unique.
```bash
aws s3 mb s3://<bucket name>
```
#### 3. Create the Template
This will look for a file called graphql.js with the export `graphqlHandler`. It creates one API endpoints:
* `/graphql` (GET and POST)
In a file called `template.yaml`:
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
GraphQL:
Type: AWS::Serverless::Function
Properties:
Handler: graphql.graphqlHandler
Runtime: nodejs8.10
Events:
GetRequest:
Type: Api
Properties:
Path: /graphql
Method: get
PostRequest:
Type: Api
Properties:
Path: /graphql
Method: post
```
#### 4. Package source code and dependencies
This will read and transform the template, created in previous step. Package and upload the artifact to the S3 bucket and generate another template for the deployment.
```sh
aws cloudformation package \
--template-file template.yaml \
--output-template-file serverless-output.yaml \
--s3-bucket <bucket-name>
```
#### 5. Deploy the API
The will create the Lambda Function and API Gateway for GraphQL. We use the stack-name `prod` to mean production but any stack name can be used.
```
aws cloudformation deploy \
--template-file serverless-output.yaml \
--stack-name prod \
--capabilities CAPABILITY_IAM
```
## Getting request info
To read information about the current request from the API Gateway event (HTTP headers, HTTP method, body, path, ...) or the current Lambda Context (Function Name, Function Version, awsRequestId, time remaning, ...) use the options function. This way they can be passed to your schema resolvers using the context option.
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ event, context }) => ({
headers: event.headers,
functionName: context.functionName,
event,
context,
})
});
exports.graphqlHandler = server.createHandler();
```
## Modifying the Lambda Response (Enable CORS)
To enable CORS the response HTTP headers need to be modified. To accomplish this use `cors` options.
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
exports.graphqlHandler = server.createHandler({
cors: {
origin: '*',
credentials: true,
},
});
```
To enable CORS response for requests with credentials (cookies, http authentication) the allow origin header must equal the request origin and the allow credential header must be set to true.
```js
const { ApolloServer, gql } = require('apollo-server-lambda');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
exports.graphqlHandler = server.createHandler({
cors: {
origin: true,
credentials: true,
},
});
```
## Principles
GraphQL Server is built with the following principles in mind:
* **By the community, for the community**: GraphQL Server's development is driven by the needs of developers
* **Simplicity**: by keeping things simple, GraphQL Server is easier to use, easier to contribute to, and more secure
* **Performance**: GraphQL Server is well-tested and production-ready - no modifications needed
Anyone is welcome to contribute to GraphQL Server, just read [CONTRIBUTING.md](https://github.com/apollographql/apollo-server/blob/master/CONTRIBUTING.md), take a look at the [roadmap](https://github.com/apollographql/apollo-server/blob/master/ROADMAP.md) and make your first PR!

View file

@ -0,0 +1,43 @@
{
"name": "apollo-server-lambda",
"version": "2.0.0-rc.3",
"description": "Production-ready Node.js GraphQL server for AWS Lambda",
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Lambda",
"Javascript"
],
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-lambda"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": ">=6"
},
"scripts": {
"clean": "rm -rf dist",
"compile": "tsc",
"prepublish": "npm run clean && npm run compile"
},
"dependencies": {
"apollo-server-core": "^2.0.0-rc.3",
"graphql-playground-html": "^1.6.0"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.6",
"apollo-server-integration-testsuite": "^2.0.0-rc.3"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
}
}

View file

@ -0,0 +1,142 @@
import * as lambda from 'aws-lambda';
import { ApolloServerBase } from 'apollo-server-core';
export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';
import { GraphQLOptions } from 'apollo-server-core';
import {
MiddlewareOptions as PlaygroundMiddlewareOptions,
renderPlaygroundPage,
RenderPageOptions as PlaygroundRenderPageOptions,
} from 'graphql-playground-html';
import { graphqlLambda } from './lambdaApollo';
export interface CreateHandlerOptions {
gui?: boolean | PlaygroundMiddlewareOptions;
cors?: {
origin?: boolean | string | string[];
methods?: string | string[];
allowedHeaders?: string | string[];
exposedHeaders?: string | string[];
credentials?: boolean;
maxAge?: number;
};
}
export class ApolloServer extends ApolloServerBase {
// This translates the arguments from the middleware into graphQL options It
// provides typings for the integration specific behavior, ideally this would
// be propagated with a generic to the super class
createGraphQLServerOptions(
event: lambda.APIGatewayProxyEvent,
context: lambda.Context,
): Promise<GraphQLOptions> {
return super.graphQLServerOptions({ event, context });
}
// Added "= { gui: undefined }" to fix "module initialization error: TypeError"
public createHandler(
{ gui, cors }: CreateHandlerOptions = { gui: undefined, cors: undefined },
) {
const guiEnabled =
!!gui || (gui === undefined && process.env.NODE_ENV !== 'production');
const corsHeaders = {};
if (cors) {
if (cors.methods) {
if (typeof cors.methods === 'string') {
corsHeaders['Access-Control-Allow-Methods'] = cors.methods;
} else if (Array.isArray(cors.methods)) {
corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(',');
}
}
if (cors.allowedHeaders) {
if (typeof cors.allowedHeaders === 'string') {
corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders;
} else if (Array.isArray(cors.allowedHeaders)) {
corsHeaders[
'Access-Control-Allow-Headers'
] = cors.allowedHeaders.join(',');
}
}
if (cors.exposedHeaders) {
if (typeof cors.exposedHeaders === 'string') {
corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders;
} else if (Array.isArray(cors.exposedHeaders)) {
corsHeaders[
'Access-Control-Expose-Headers'
] = cors.exposedHeaders.join(',');
}
}
if (cors.credentials) {
corsHeaders['Access-Control-Allow-Credentials'] = 'true';
}
if (cors.maxAge) {
corsHeaders['Access-Control-Max-Age'] = cors.maxAge;
}
}
return (
event: lambda.APIGatewayProxyEvent,
context: lambda.Context,
callback: lambda.APIGatewayProxyCallback,
) => {
if (cors && cors.origin) {
if (typeof cors.origin === 'string') {
corsHeaders['Access-Control-Allow-Origin'] = cors.origin;
} else if (
typeof cors.origin === 'boolean' ||
(Array.isArray(cors.origin) &&
cors.origin.includes(
event.headers['Origin'] || event.headers['origin'],
))
) {
corsHeaders['Access-Control-Allow-Origin'] =
event.headers['Origin'] || event.headers['origin'];
}
}
if (guiEnabled && event.httpMethod === 'GET') {
const acceptHeader = event.headers['Accept'] || event.headers['accept'];
if (acceptHeader && acceptHeader.includes('text/html')) {
const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
endpoint: event.requestContext.path,
...(typeof gui === 'boolean' ? {} : gui),
version: '1.7.0',
};
return callback(null, {
body: renderPlaygroundPage(playgroundRenderPageOptions),
statusCode: 200,
headers: {
'Content-Type': 'text/html',
...corsHeaders,
},
});
}
}
const callbackFilter: lambda.APIGatewayProxyCallback = (
error,
result,
) => {
callback(error, {
...result,
headers: {
...result.headers,
...corsHeaders,
},
});
};
graphqlLambda(this.createGraphQLServerOptions.bind(this))(
event,
context,
callbackFilter,
);
};
}
}

View file

@ -0,0 +1,15 @@
// Expose types which can be used by both middleware flavors.
export { GraphQLOptions, gql } from 'apollo-server-core';
export {
ApolloError,
toApolloError,
SyntaxError,
ValidationError,
AuthenticationError,
ForbiddenError,
UserInputError,
} from 'apollo-server-core';
// ApolloServer integration
export { ApolloServer, CreateHandlerOptions } from './ApolloServer';

View file

@ -0,0 +1,57 @@
import { ApolloServer } from './ApolloServer';
import testSuite, {
schema as Schema,
CreateAppOptions,
} from 'apollo-server-integration-testsuite';
import { Config } from 'apollo-server-core';
import 'mocha';
import * as url from 'url';
import { IncomingMessage, ServerResponse } from 'http';
const createLambda = (options: CreateAppOptions = {}) => {
const server = new ApolloServer(
(options.graphqlOptions as Config) || { schema: Schema },
);
const handler = server.createHandler();
return (req: IncomingMessage, res: ServerResponse) => {
// return 404 if path is /bogus-route to pass the test, lambda doesn't have paths
if (req.url.includes('/bogus-route')) {
res.statusCode = 404;
return res.end();
}
let body = '';
req.on('data', chunk => (body += chunk));
req.on('end', () => {
const urlObject = url.parse(req.url, true);
const event = {
httpMethod: req.method,
body: body,
path: req.url,
queryStringParameters: urlObject.query,
requestContext: {
path: urlObject.pathname,
},
headers: req.headers,
};
const callback = (error, result) => {
if (error) throw error;
res.statusCode = result.statusCode;
for (let key in result.headers) {
if (result.headers.hasOwnProperty(key)) {
res.setHeader(key, result.headers[key]);
}
}
res.write(result.body);
res.end();
};
handler(event as any, {} as any, callback);
});
};
};
describe('integration:Lambda', () => {
testSuite(createLambda);
});

View file

@ -0,0 +1,70 @@
import * as lambda from 'aws-lambda';
import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
} from 'apollo-server-core';
export interface LambdaGraphQLOptionsFunction {
(event: lambda.APIGatewayProxyEvent, context: lambda.Context):
| GraphQLOptions
| Promise<GraphQLOptions>;
}
export function graphqlLambda(
options: GraphQLOptions | LambdaGraphQLOptionsFunction,
): lambda.APIGatewayProxyHandler {
if (!options) {
throw new Error('Apollo Server requires options.');
}
if (arguments.length > 1) {
throw new Error(
`Apollo Server expects exactly one argument, got ${arguments.length}`,
);
}
const graphqlHandler: lambda.APIGatewayProxyHandler = (
event,
context,
callback,
): void => {
if (event.httpMethod === 'POST' && !event.body) {
return callback(null, {
body: 'POST body missing.',
statusCode: 500,
});
}
runHttpQuery([event, context], {
method: event.httpMethod,
options: options,
query:
event.httpMethod === 'POST'
? JSON.parse(event.body)
: (event.queryStringParameters as any),
request: {
url: event.path,
method: event.httpMethod,
headers: event.headers as any,
},
}).then(
({ graphqlResponse, responseInit }) => {
callback(null, {
body: graphqlResponse,
statusCode: 200,
headers: responseInit.headers,
});
},
(error: HttpQueryError) => {
if ('HttpQueryError' !== error.name) return callback(error);
callback(null, {
body: error.message,
statusCode: error.statusCode,
headers: error.headers,
});
},
);
};
return graphqlHandler;
}

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-memcached",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
@ -23,8 +23,8 @@
"node": ">=6"
},
"dependencies": {
"apollo-server-env": "^2.0.0-rc.0",
"apollo-server-caching": "^2.0.0-rc.0",
"apollo-server-caching": "^2.0.0-rc.3",
"apollo-server-env": "^2.0.0-rc.3",
"memcached": "^2.2.2"
},
"devDependencies": {

View file

@ -1,5 +1,5 @@
import { KeyValueCache } from 'apollo-server-caching';
import Memcached from 'memcached';
import * as Memcached from 'memcached';
import { promisify } from 'util';
export class MemcachedCache implements KeyValueCache {

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-redis",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
@ -23,8 +23,8 @@
"node": ">=6"
},
"dependencies": {
"apollo-server-caching": "^2.0.0-rc.0",
"apollo-server-env": "^2.0.0-rc.0",
"apollo-server-caching": "^2.0.0-rc.3",
"apollo-server-env": "^2.0.0-rc.3",
"redis": "^2.8.0"
},
"devDependencies": {

View file

@ -1,5 +1,5 @@
import { KeyValueCache } from 'apollo-server-caching';
import Redis from 'redis';
import * as Redis from 'redis';
import { promisify } from 'util';
export class RedisCache implements KeyValueCache {

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.3",
"description": "Production ready GraphQL Server",
"author": "opensource@apollographql.com",
"main": "dist/index.js",
@ -33,8 +33,8 @@
},
"typings": "dist/index.d.ts",
"dependencies": {
"apollo-server-core": "^2.0.0-rc.0",
"apollo-server-express": "^2.0.0-rc.0",
"apollo-server-core": "^2.0.0-rc.3",
"apollo-server-express": "^2.0.0-rc.3",
"express": "^4.0.0",
"graphql-subscriptions": "^0.5.8"
}

View file

@ -1,7 +1,7 @@
import { expect } from 'chai';
import 'mocha';
import request from 'request';
import * as request from 'request';
import { createApolloFetch } from 'apollo-fetch';
import { gql, ApolloServer } from './index';

View file

@ -1,10 +1,10 @@
// Note: express is only used if you use the ApolloServer.listen API to create
// an express app for you instead of registerServer (which you might not even
// an express app for you instead of applyMiddleware (which you might not even
// use with express). The dependency is unused otherwise, so don't worry if
// you're not using express or your version doesn't quite match up.
import express from 'express';
import http from 'http';
import net from 'net';
import * as express from 'express';
import * as http from 'http';
import * as net from 'net';
import { ApolloServer as ApolloServerBase } from 'apollo-server-express';
export { GraphQLOptions, GraphQLExtension, gql } from 'apollo-server-core';
@ -59,7 +59,7 @@ export class ApolloServer extends ApolloServerBase {
// object, so we have to create it.
const app = express();
//provide generous values for the getting started experience
// provide generous values for the getting started experience
this.applyMiddleware({
app,
path: '/',

View file

@ -28,3 +28,5 @@ require('../packages/apollo-server-express/dist/datasource.test');
require('../packages/apollo-server-hapi/dist/hapiApollo.test') && // Hapi 17 is 8.9+
require('../packages/apollo-server-hapi/dist/ApolloServer.test.js');
require('../packages/apollo-server-express/dist/apolloServerHttp.test');
require('../packages/apollo-server-lambda/dist/lambdaApollo.test');

View file

@ -3,7 +3,6 @@
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true,