Merge pull request #1063 from apollographql/refactor-2.0

Apollo Server 2.0
This commit is contained in:
Evans Hauser 2018-06-01 13:59:53 -07:00 committed by GitHub
commit 639b104232
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 3773 additions and 718 deletions

View file

@ -36,10 +36,6 @@ jobs:
# Platform tests, each with the same tests but different platform or version.
# The docker tag represents the Node.js version and the full list is available
# at https://hub.docker.com/r/circleci/node/.
Node.js 4:
docker: [ { image: 'circleci/node:4' } ]
<<: *common_test_steps
Node.js 6:
docker: [ { image: 'circleci/node:6' } ]
<<: *common_test_steps
@ -79,8 +75,6 @@ workflows:
version: 2
Build and Test:
jobs:
- Node.js 4:
<<: *ignore_doc_branches
- Node.js 6:
<<: *ignore_doc_branches
- Node.js 8:

View file

@ -4,6 +4,9 @@ All of the packages in the `apollo-server` repo are released with the same versi
### vNEXT
* 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)
### v1.3.6
* Recognize requests with Apollo Persisted Queries and return `PersistedQueryNotSupported` to the client instead of a confusing error. [PR #982](https://github.com/apollographql/apollo-server/pull/982)

View file

@ -235,7 +235,8 @@ store.put('query testquery{ testString }');
graphqlOptions = {
schema: Schema,
formatParams(params) {
params['query'] = store.get(params.operationName);
params['parsedQuery'] = store.get(params.operationName);
delete params['queryString']; // Or throw if this is provided.
return params;
},
};

View file

@ -4,7 +4,6 @@ subtitle: Apollo Server
description: A guide to using Apollo Server.
versions:
- '1'
- '2'
sidebar_categories:
null:
- index

View file

@ -107,13 +107,16 @@ const GraphQLOptions = {
validationRules?: Array<ValidationRule>,
// a function applied to each graphQL execution result
formatResponse?: Function
formatResponse?: Function,
// a custom default field resolver
fieldResolver?: Function
fieldResolver?: Function,
// a boolean that will print additional debug logging if execution errors occur
debug?: boolean
debug?: boolean,
// (optional) extra GraphQL extensions from graphql-extensions
extensions?: Array<() => GraphQLExtension>
}
```

View file

@ -12,7 +12,6 @@
"tag: internal": ":house: Internal"
}
},
"packages": [
"packages/*"
]
"hoist": true,
"packages": ["packages/*"]
}

View file

@ -34,9 +34,9 @@
},
"devDependencies": {
"@types/chai": "4.1.3",
"@types/graphql": "^0.13.1",
"@types/mocha": "5.2.0",
"@types/node": "9.6.6",
"@types/sinon": "4.3.0",
"@types/sinon": "4.3.3",
"chai": "4.1.2",
"graphql": "0.13.2",
"husky": "0.14.3",
@ -47,7 +47,7 @@
"prettier": "1.12.1",
"prettier-check": "2.0.0",
"remap-istanbul": "0.11.1",
"sinon": "4.5.0",
"sinon": "5.0.7",
"supertest": "3.0.0",
"supertest-as-promised": "4.0.2",
"typescript": "2.8.3"

View file

@ -25,15 +25,15 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"devDependencies": {
"@adonisjs/bodyparser": "2.0.2",
"@adonisjs/bodyparser": "2.0.3",
"@adonisjs/fold": "4.0.8",
"@adonisjs/framework": "4.0.31",
"@adonisjs/sink": "1.0.16",
"@types/graphql": "0.12.7",
"@types/node": "^10.1.2",
"apollo-server-integration-testsuite": "^1.3.6"
},
"typings": "dist/index.d.ts",

View file

@ -3,6 +3,7 @@ import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
convertNodeHttpToRequest,
} from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
@ -34,6 +35,7 @@ export function graphqlAdonis(
method,
options,
query,
request: convertNodeHttpToRequest(request.request),
}).then(
gqlResponse => {
response.type('application/json');

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -25,11 +25,11 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"devDependencies": {
"@types/graphql": "0.12.7",
"@types/node": "^10.1.2",
"apollo-server-integration-testsuite": "^1.3.6",
"azure-functions-typescript": "0.0.1"
},

View file

@ -58,6 +58,7 @@ export function graphqlAzureFunctions(
method: request.method,
options: options,
query: request.method === 'POST' ? request.body : request.query,
request,
};
if (queryRequest.query && typeof queryRequest.query === 'string') {

View file

@ -2,9 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"],
"types": ["@types/node"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -1,7 +1,7 @@
{
"name": "graphql-server-module-graphiql",
"version": "1.3.4",
"description": "GraphiQL renderer for Apollo GraphQL Server",
"name": "apollo-server-cloudflare",
"version": "1.0.0-beta.1",
"description": "Production-ready Node.js GraphQL server for Cloudflare workers",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
@ -9,21 +9,22 @@
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-module-graphiql"
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-cloudflare-workers"
},
"keywords": [
"GraphQL",
"GraphiQL",
"Apollo",
"Server",
"Cloudflare",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"typings": "dist/index.d.ts",

View file

@ -0,0 +1,18 @@
import { graphqlCloudflare } from './cloudflareApollo';
import { ApolloServerBase } from 'apollo-server-core';
interface FetchEvent extends Event {
respondWith: (result: Promise<ResponseInit>) => void;
request: RequestInit;
}
export class ApolloServer extends ApolloServerBase {
public async listen() {
const graphql = this.graphQLServerOptionsForRequest.bind(this);
addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(graphqlCloudflare(graphql)(event.request));
});
return await { url: '', port: null };
}
}

View file

@ -0,0 +1,68 @@
import * as url from 'url';
import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
} from 'apollo-server-core';
// import * as GraphiQL from 'apollo-server-module-graphiql';
// Design principles:
// - You can issue a GET or POST with your query.
// - simple, fast and secure
//
export function graphqlCloudflare(options: GraphQLOptions) {
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 = async (req: RequestInit): Promise<Response> => {
const url = new URL((req as Request).url);
const query =
req.method === 'POST'
? await (req as Request).json()
: {
query: url.searchParams.get('query'),
variables: url.searchParams.get('variables'),
operationName: url.searchParams.get('operationName'),
extensions: url.searchParams.get('extensions'),
};
return runHttpQuery([req], {
method: req.method,
options: options,
query,
request: req as Request,
}).then(
gqlResponse =>
new Response(gqlResponse, {
status: 200,
headers: { 'content-type': 'application/json' },
}),
(error: HttpQueryError) => {
if ('HttpQueryError' !== error.name) throw error;
const res = new Response(error.message, {
status: error.statusCode,
headers: { 'content-type': 'application/json' },
});
if (error.headers) {
Object.keys(error.headers).forEach(header => {
res.headers[header] = error.headers[header];
});
}
return res;
},
);
};
return graphqlHandler;
}

View file

@ -0,0 +1,2 @@
export { graphqlCloudflare } from './cloudflareApollo';
export { ApolloServer } from './ApolloServer';

View file

@ -4,5 +4,6 @@
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,12 @@
# Changelog
### vNEXT
* `apollo-server-core`: accept `Request` object in `runQuery` [PR#1108](https://github.com/apollographql/apollo-server/pull/1108)
* `apollo-server-core`: move query parse into runQuery and no longer accept GraphQL AST over the wire [PR#1097](https://github.com/apollographql/apollo-server/pull/1097)
* `apollo-server-core`: custom errors allow instanceof checks [PR#1074](https://github.com/apollographql/apollo-server/pull/1074)
* `apollo-server-core`: move subscriptions options into listen [PR#1059](https://github.com/apollographql/apollo-server/pull/1059)
* `apollo-server-core`: Replace console.error with logFunction for opt-in logging [PR #1024](https://github.com/apollographql/apollo-server/pull/1024)
* `apollo-server-core`: context creation can be async and errors are formatted to include error code [PR #1024](https://github.com/apollographql/apollo-server/pull/1024)
* `apollo-server-core`: add `mocks` parameter to the base constructor(applies to all variants) [PR#1017](https://github.com/apollographql/apollo-server/pull/1017)
* `apollo-server-core`: Remove printing of stack traces with `debug` option and include response in logging function[PR#1018](https://github.com/apollographql/apollo-server/pull/1018)

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-core",
"version": "1.3.6",
"version": "2.0.0-beta.2",
"description": "Core engine for Apollo GraphQL server",
"main": "dist/index.js",
"scripts": {
@ -26,21 +26,34 @@
"homepage": "https://github.com/apollographql/apollo-server#readme",
"devDependencies": {
"@types/fibers": "0.0.30",
"@types/graphql": "0.12.7",
"@types/graphql": "^0.13.1",
"@types/node": "^10.1.2",
"@types/node-fetch": "^1.6.9",
"@types/ws": "^4.0.2",
"apollo-engine": "^1.1.1",
"apollo-fetch": "^0.7.0",
"fibers": "1.0.15",
"graphql-tag": "^2.9.2",
"meteor-promise": "0.8.6",
"mock-req": "^0.2.0",
"typescript": "2.8.3"
},
"peerDependencies": {
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0"
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
},
"dependencies": {
"apollo-cache-control": "^0.1.0",
"apollo-tracing": "^0.1.0",
"graphql-extensions": "^0.0.x"
"apollo-cache-control": "^0.1.1",
"apollo-engine-reporting": "0.0.0-beta.8",
"apollo-tracing": "^0.2.0-beta.0",
"graphql-extensions": "0.1.0-beta.7",
"graphql-subscriptions": "^0.5.8",
"graphql-tools": "^3.0.2",
"node-fetch": "^2.1.2",
"subscriptions-transport-ws": "^0.9.9",
"ws": "^5.1.1"
}
}

View file

@ -0,0 +1,825 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai';
import { stub } from 'sinon';
import * as http from 'http';
import * as net from 'net';
import * as MockReq from 'mock-req';
import 'mocha';
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLError,
ValidationContext,
FieldDefinitionNode,
} from 'graphql';
import { PubSub } from 'graphql-subscriptions';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import * as WebSocket from 'ws';
Object.assign(global, {
WebSocket: WebSocket,
});
import { createApolloFetch } from 'apollo-fetch';
import { ApolloServerBase } from './ApolloServer';
import { AuthenticationError } from './errors';
import { runHttpQuery } from './runHttpQuery';
import gqlTag from 'graphql-tag';
let gql = String.raw;
const INTROSPECTION_QUERY = gql`
{
__schema {
directives {
name
}
}
}
`;
const TEST_STRING_QUERY = gql`
{
testString
}
`;
const queryType = new GraphQLObjectType({
name: 'QueryType',
fields: {
testString: {
type: GraphQLString,
resolve() {
return 'test string';
},
},
},
});
const schema = new GraphQLSchema({
query: queryType,
});
function createHttpServer(server) {
return http.createServer(async (req, res) => {
const result = {};
let body: any = [];
req
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// At this point, we have the headers, method, url and body, and can now
// do whatever we need to in order to respond to this request.
runHttpQuery([req, res], {
method: req.method,
options: server.graphQLServerOptionsForRequest(req as any),
query: JSON.parse(body),
request: new MockReq(),
})
.then(gqlResponse => {
res.setHeader('Content-Type', 'application/json');
res.setHeader(
'Content-Length',
Buffer.byteLength(gqlResponse, 'utf8').toString(),
);
res.write(gqlResponse);
res.end();
})
.catch(error => {
res.write(error.message);
res.end();
});
});
});
}
describe('ApolloServerBase', () => {
describe('constructor', () => {
describe('validation rules', () => {
it('accepts additional rules', async () => {
const NoTestString = (context: ValidationContext) => ({
Field(node: FieldDefinitionNode) {
if (node.name.value === 'testString') {
context.reportError(
new GraphQLError('Not allowed to use', [node]),
);
}
},
});
const server = new ApolloServerBase({
schema,
validationRules: [NoTestString],
introspection: false,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const introspectionResult = await apolloFetch({
query: INTROSPECTION_QUERY,
});
expect(introspectionResult.data, 'data should not exist').not.to.exist;
expect(introspectionResult.errors, 'errors should exist').to.exist;
const result = await apolloFetch({ query: TEST_STRING_QUERY });
expect(result.data, 'data should not exist').not.to.exist;
expect(result.errors, 'errors should exist').to.exist;
await server.stop();
});
it('allows introspection by default', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const server = new ApolloServerBase({
schema,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: INTROSPECTION_QUERY });
expect(result.data, 'data should not exist').to.exist;
expect(result.errors, 'errors should exist').not.to.exist;
process.env.NODE_ENV = nodeEnv;
await server.stop();
});
it('prevents introspection by default during production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const server = new ApolloServerBase({
schema,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: INTROSPECTION_QUERY });
expect(result.data, 'data should not exist').not.to.exist;
expect(result.errors, 'errors should exist').to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal(
'GRAPHQL_VALIDATION_FAILED',
);
process.env.NODE_ENV = nodeEnv;
await server.stop();
});
it('allows introspection to be enabled explicitly', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const server = new ApolloServerBase({
schema,
introspection: true,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: INTROSPECTION_QUERY });
expect(result.data, 'data should not exist').to.exist;
expect(result.errors, 'errors should exist').not.to.exist;
process.env.NODE_ENV = nodeEnv;
await server.stop();
});
});
describe('schema creation', () => {
it('accepts typeDefs and resolvers', async () => {
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = { Query: { hello: () => 'hi' } };
const server = new ApolloServerBase({
typeDefs,
resolvers,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: uri } = await server.listen();
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;
await server.stop();
});
it('uses schema over resolvers + typeDefs', async () => {
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = { Query: { hello: () => 'hi' } };
const server = new ApolloServerBase({
typeDefs,
resolvers,
schema,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const typeDefResult = await apolloFetch({ query: '{hello}' });
expect(typeDefResult.data, 'data should not exist').not.to.exist;
expect(typeDefResult.errors, 'errors should exist').to.exist;
const result = await apolloFetch({ query: '{testString}' });
expect(result.data).to.deep.equal({ testString: 'test string' });
expect(result.errors, 'errors should exist').not.to.exist;
await server.stop();
});
it('allows mocks as boolean', async () => {
const typeDefs = gql`
type Query {
hello: String
}
`;
const server = new ApolloServerBase({
typeDefs,
mocks: true,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.data).to.deep.equal({ hello: 'Hello World' });
expect(result.errors, 'errors should exist').not.to.exist;
await server.stop();
});
it('allows mocks as an object', async () => {
const typeDefs = gql`
type Query {
hello: String
}
`;
const server = new ApolloServerBase({
typeDefs,
mocks: { String: () => 'mock city' },
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.data).to.deep.equal({ hello: 'mock city' });
expect(result.errors, 'errors should exist').not.to.exist;
await server.stop();
});
});
});
describe('lifecycle', () => {
it('defers context eval with thunk until after options creation', async () => {
const uniqueContext = { key: 'major' };
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: (parent, args, context) => {
expect(context).to.equal(Promise.resolve(uniqueContext));
return 'hi';
},
},
};
const spy = stub().returns({});
const server = new ApolloServerBase({
typeDefs,
resolvers,
context: spy,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });
expect(spy.notCalled).true;
await apolloFetch({ query: '{hello}' });
expect(spy.calledOnce).true;
await apolloFetch({ query: '{hello}' });
expect(spy.calledTwice).true;
await server.stop();
});
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: (parent, args, context) => {
throw Error('never get here');
},
},
};
const server = new ApolloServerBase({
typeDefs,
resolvers,
context: ({ req }) => {
throw new AuthenticationError('valid result');
},
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: uri } = await server.listen();
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;
await server.stop();
});
it('propogates error codes in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const server = new ApolloServerBase({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
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;
await server.stop();
});
it('propogates error codes with null response in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const server = new ApolloServerBase({
typeDefs: gql`
type Query {
error: String!
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
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;
await server.stop();
});
});
describe('engine', () => {
it('creates ApolloEngine instance when api key is present', async () => {
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hi',
},
};
const server = new ApolloServerBase({
typeDefs,
resolvers,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});
const { url: engineUri, port: enginePort } = await server.listen({
engineProxy: {
apiKey: 'service:apollographql-6872:D6HRzC5ykWElYO3A2od1uA',
logging: {
level: 'ERROR',
},
},
http: {
port: 4242,
},
});
expect(enginePort).to.equal(4242);
//Check engine responding
const engineApolloFetch = createApolloFetch({ uri: engineUri });
const engineResult = await engineApolloFetch({ query: '{hello}' });
expect(engineResult.data).to.deep.equal({ hello: 'hi' });
expect(engineResult.errors, 'errors should not exist').not.to.exist;
expect(engineResult.extensions, 'extensions should exist').not.to.exist;
//only windows returns a string https://github.com/nodejs/node/issues/12895
const { address, port } = httpServer.address() as net.AddressInfo;
expect(enginePort).not.to.equal(port);
const uri = `http://${address}:${port}/`;
//Check origin server responding and includes extensions
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.data).to.deep.equal({ hello: 'hi' });
expect(result.errors, 'errors should not exist').not.to.exist;
expect(result.extensions, 'extensions should exist').to.exist;
await server.stop();
expect(httpServer.listening).false;
});
});
describe('subscritptions', () => {
const SOMETHING_CHANGED_TOPIC = 'something_changed';
const pubsub = new PubSub();
let server: ApolloServerBase;
let subscription;
function createEvent(num) {
return setTimeout(
() =>
pubsub.publish(SOMETHING_CHANGED_TOPIC, {
num,
}),
num + 10,
);
}
afterEach(async () => {
if (server) {
try {
await server.stop();
} catch (e) {}
server = null;
}
if (subscription) {
try {
await subscription.unsubscribe();
} catch (e) {}
subscription = null;
}
});
it('enables subscriptions by default', done => {
const typeDefs = gql`
type Query {
hi: String
}
type Subscription {
num: Int
}
`;
const query = gqlTag(gql`
subscription {
num
}
`);
const resolvers = {
Query: {
hi: () => 'here to placate graphql-js',
},
Subscription: {
num: {
subscribe: () => {
createEvent(1);
createEvent(2);
createEvent(3);
return pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC);
},
},
},
};
server = new ApolloServerBase({
typeDefs,
resolvers,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
server.listen({}).then(({ url: uri, port }) => {
const client = new SubscriptionClient(
`ws://localhost:${port}${server.subscriptionsPath}`,
{},
WebSocket,
);
const observable = client.request({ query });
let i = 1;
subscription = observable.subscribe({
next: ({ data }) => {
try {
expect(data.num).to.equal(i);
if (i === 3) {
done();
}
i++;
} catch (e) {
done(e);
}
},
error: done,
complete: () => {
done(new Error('should not complete'));
},
});
});
});
it('disables subscritpions when option set to false', done => {
const typeDefs = gql`
type Query {
"graphql-js forces there to be a query type"
hi: String
}
type Subscription {
num: Int
}
`;
const query = gqlTag(gql`
subscription {
num
}
`);
const resolvers = {
Query: {
hi: () => 'here to placate graphql-js',
},
Subscription: {
num: {
subscribe: () => {
createEvent(1);
return pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC);
},
},
},
};
server = new ApolloServerBase({
typeDefs,
resolvers,
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
server
.listen({
subscriptions: false,
})
.then(({ url: uri, port }) => {
const client = new SubscriptionClient(
`ws://localhost:${port}${server.subscriptionsPath}`,
{},
WebSocket,
);
const observable = client.request({ query });
let i = 1;
subscription = observable.subscribe({
next: () => {
done(new Error('should not call next'));
},
error: () => {
done(new Error('should not notify of error'));
},
complete: () => {
done(new Error('should not complete'));
},
});
//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((err: Error) => {
done();
});
});
});
it('accepts subscriptions configuration', done => {
const onConnect = stub().callsFake(connectionParams => ({
...connectionParams,
}));
const typeDefs = gql`
type Query {
hi: String
}
type Subscription {
num: Int
}
`;
const query = gqlTag(gql`
subscription {
num
}
`);
const resolvers = {
Query: {
hi: () => 'here to placate graphql-js',
},
Subscription: {
num: {
subscribe: () => {
createEvent(1);
createEvent(2);
createEvent(3);
return pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC);
},
},
},
};
server = new ApolloServerBase({
typeDefs,
resolvers,
});
const httpServer = createHttpServer(server);
const path = '/sub';
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
server
.listen({
subscriptions: { onConnect, path },
})
.then(({ url: uri, port }) => {
expect(onConnect.notCalled).true;
expect(server.subscriptionsPath).to.equal(path);
const client = new SubscriptionClient(
`ws://localhost:${port}${server.subscriptionsPath}`,
{},
WebSocket,
);
const observable = client.request({ query });
let i = 1;
subscription = observable.subscribe({
next: ({ data }) => {
try {
expect(onConnect.calledOnce).true;
expect(data.num).to.equal(i);
if (i === 3) {
done();
}
i++;
} catch (e) {
done(e);
}
},
error: done,
complete: () => {
done(new Error('should not complete'));
},
});
})
.catch(done);
});
});
});

View file

@ -0,0 +1,381 @@
import {
makeExecutableSchema,
addMockFunctionsToSchema,
IResolvers,
mergeSchemas,
} from 'graphql-tools';
import { Server as HttpServer } from 'http';
import {
execute,
GraphQLSchema,
subscribe,
ExecutionResult,
GraphQLError,
GraphQLResolveInfo,
ValidationContext,
FieldDefinitionNode,
} from 'graphql';
import { GraphQLExtension } from 'graphql-extensions';
import { TracingExtension } from 'apollo-tracing';
import { CacheControlExtension } from 'apollo-cache-control';
import { EngineReportingAgent } from 'apollo-engine-reporting';
import { ApolloEngine } from 'apollo-engine';
import {
SubscriptionServer,
ExecutionParams,
} from 'subscriptions-transport-ws';
import { formatApolloErrors } from './errors';
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions';
import { LogFunction, LogAction, LogStep } from './logging';
import {
Config,
ListenOptions,
MiddlewareOptions,
RegistrationOptions,
ServerInfo,
Context,
ContextFunction,
SubscriptionServerOptions,
} from './types';
const NoIntrospection = (context: ValidationContext) => ({
Field(node: FieldDefinitionNode) {
if (node.name.value === '__schema' || node.name.value === '__type') {
context.reportError(
new GraphQLError(
'GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production',
[node],
),
);
}
},
});
export class ApolloServerBase<Request = RequestInit> {
public disableTools: boolean;
// set in the listen function if subscriptions are enabled
public subscriptionsPath: string;
public requestOptions: Partial<GraphQLOptions<any>>;
private schema: GraphQLSchema;
private context?: Context | ContextFunction;
private graphqlPath: string = '/graphql';
private engineReportingAgent?: EngineReportingAgent;
private engineProxy: ApolloEngine;
private extensions: Array<() => GraphQLExtension>;
private http?: HttpServer;
private subscriptionServer?: SubscriptionServer;
protected getHttp: () => HttpServer;
constructor(config: Config<Request>) {
const {
context,
resolvers,
schema,
schemaDirectives,
typeDefs,
introspection,
mocks,
extensions,
engine,
...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
const env = process.env.NODE_ENV;
const isDev = env !== 'production' && env !== 'test';
// if this is local dev, we want graphql gui and introspection to be turned on
// in production, you can manually turn these on by passing { introspection: true }
// to the constructor of ApolloServer
// we use this.disableTools to track this internally for later use when
// constructing middleware by frameworks
if (typeof introspection === 'boolean') this.disableTools = !introspection;
else this.disableTools = !isDev;
if (this.disableTools) {
const noIntro = [NoIntrospection];
requestOptions.validationRules = requestOptions.validationRules
? requestOptions.validationRules.concat(noIntro)
: noIntro;
}
this.requestOptions = requestOptions;
this.context = context;
const enhancedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
enhancedTypeDefs.push(`scalar Upload`);
this.schema = schema
? schema
: makeExecutableSchema({
typeDefs: enhancedTypeDefs.join('\n'),
schemaDirectives,
resolvers,
});
if (mocks) {
addMockFunctionsToSchema({
schema: this.schema,
preserveResolvers: true,
mocks: typeof mocks === 'boolean' ? {} : mocks,
});
}
// Note: if we're using engineproxy (directly or indirectly), we will extend
// this when we listen.
this.extensions = [];
if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
this.engineReportingAgent = new EngineReportingAgent(
engine === true ? {} : engine,
);
// Let's keep this extension first so it wraps everything.
this.extensions.push(() => this.engineReportingAgent.newExtension());
}
if (extensions) {
this.extensions = [...this.extensions, ...extensions];
}
}
public use({ getHttp, path }: RegistrationOptions) {
// we need to delay when we actually get the http server
// until we move into the listen function
this.getHttp = getHttp;
this.graphqlPath = path;
}
public enhanceSchema(
schema: GraphQLSchema | { typeDefs: string; resolvers: IResolvers },
) {
this.schema = mergeSchemas({
schemas: [
this.schema,
'typeDefs' in schema ? schema['typeDefs'] : schema,
],
resolvers: 'resolvers' in schema ? [, schema['resolvers']] : {},
});
}
public listen(opts: ListenOptions = {}): Promise<ServerInfo> {
this.http = this.getHttp();
const options = {
...opts,
http: {
port: process.env.PORT || 4000,
...opts.http,
},
};
if (opts.subscriptions !== false) {
let config: SubscriptionServerOptions;
if (
opts.subscriptions === true ||
typeof opts.subscriptions === 'undefined'
) {
config = {
path: this.graphqlPath,
};
} else if (typeof opts.subscriptions === 'string') {
config = { path: opts.subscriptions };
} else {
config = { path: this.graphqlPath, ...opts.subscriptions };
}
this.subscriptionsPath = config.path;
this.subscriptionServer = this.createSubscriptionServer(
this.http,
config,
);
}
if (opts.engineProxy || opts.engineInRequestPath) this.createEngine(opts);
return new Promise((resolve, reject) => {
if (this.engineProxy) {
this.engineProxy.listen(
{
graphqlPaths: [this.graphqlPath],
port: options.http.port,
httpServer: this.http,
launcherOptions: options.engineLauncherOptions,
},
() => {
this.engineProxy.engineListeningAddress.url = require('url').resolve(
this.engineProxy.engineListeningAddress.url,
this.graphqlPath,
);
resolve(this.engineProxy.engineListeningAddress);
},
);
this.engineProxy.on('error', reject);
return;
}
// all options for http listeners
// https://nodejs.org/api/net.html#net_server_listen_options_callback
// https://github.com/apollographql/apollo-server/pull/979/files/33ea0c92a1e4e76c8915ff08806f15dae391e1f0#discussion_r184470435
// https://github.com/apollographql/apollo-server/pull/979#discussion_r184471445
function listenCallback() {
const listeningAddress: any = this.http.address();
// Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
// corresponding loopback ip. Note that the url field we're setting is
// primarily for consumption by our test suite. If this heuristic is
// wrong for your use case, explicitly specify a frontend host (in the
// `frontends.host` field in your engine config, or in the `host`
// option to ApolloServer.listen).
let hostForUrl = listeningAddress.address;
if (
listeningAddress.address === '' ||
listeningAddress.address === '::'
)
hostForUrl = 'localhost';
listeningAddress.url = require('url').format({
protocol: 'http',
hostname: hostForUrl,
port: listeningAddress.port,
pathname: this.graphqlPath,
});
resolve(listeningAddress);
}
if (options.http.handle) {
this.http.listen(
options.http.handle,
options.http.backlog,
listenCallback.bind(this),
);
} else {
this.http.listen(options.http, listenCallback.bind(this));
}
});
}
public async stop() {
if (this.engineProxy) await this.engineProxy.stop();
if (this.subscriptionServer) await this.subscriptionServer.close();
if (this.http) await new Promise(s => this.http.close(s));
}
private createSubscriptionServer(
server: HttpServer,
config: SubscriptionServerOptions,
) {
const { onDisconnect, onConnect, keepAlive, path } = config;
return SubscriptionServer.create(
{
schema: this.schema,
execute,
subscribe,
onConnect: onConnect
? onConnect
: (connectionParams: Object) => ({ ...connectionParams }),
onDisconnect: onDisconnect,
onOperation: async (_: string, connection: ExecutionParams) => {
connection.formatResponse = (value: ExecutionResult) => ({
...value,
errors:
value.errors &&
formatApolloErrors([...value.errors], {
formatter: this.requestOptions.formatError,
debug: this.requestOptions.debug,
logFunction: this.requestOptions.logFunction,
}),
});
let context: Context = this.context ? this.context : { connection };
try {
context =
typeof this.context === 'function'
? await this.context({ connection })
: context;
} catch (e) {
throw formatApolloErrors([e], {
formatter: this.requestOptions.formatError,
debug: this.requestOptions.debug,
logFunction: this.requestOptions.logFunction,
})[0];
}
return { ...connection, context };
},
keepAlive,
},
{
server,
path,
},
);
}
private createEngine({ engineInRequestPath, engineProxy }: ListenOptions) {
// only access this onces as its slower on node
const { ENGINE_API_KEY, ENGINE_CONFIG } = process.env;
if (engineProxy === false && (ENGINE_API_KEY || ENGINE_CONFIG)) {
console.warn(
'engine is set to false when creating ApolloServer but either ENGINE_CONFIG or ENGINE_API_KEY was found in the environment',
);
}
let ApolloEngine;
if (engineProxy) {
// detect engine if it is set to true or has a config, and possibly load it
try {
ApolloEngine = require('apollo-engine').ApolloEngine;
} catch (e) {
console.warn(`ApolloServer was unable to load Apollo Engine yet engine was configured in the options when creating this ApolloServer? To fix this, run the following command:
npm install apollo-engine --save
`);
}
this.engineProxy = new ApolloEngine(
typeof engineProxy === 'boolean' ? undefined : engineProxy,
);
}
// XXX should this allow for header overrides from graphql-playground?
if (this.engineProxy || engineInRequestPath) {
this.extensions.push(() => new TracingExtension());
// XXX provide a way to pass options to CacheControlExtension (eg
// defaultMaxAge)
this.extensions.push(() => new CacheControlExtension());
}
}
graphQLServerOptionsForRequest(request: Request) {
let context: Context = this.context ? this.context : { request };
try {
context =
typeof this.context === 'function'
? this.context({ req: request })
: context;
} catch (error) {
//Defer context error resolution to inside of runQuery
context = () => {
throw error;
};
}
return {
schema: this.schema,
extensions: this.extensions,
context,
// allow overrides from options
...this.requestOptions,
};
}
}

View file

@ -0,0 +1,174 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai';
import { stub, spy } from 'sinon';
import 'mocha';
import { GraphQLError } from 'graphql';
import {
ApolloError,
formatApolloErrors,
AuthenticationError,
ForbiddenError,
ValidationError,
SyntaxError,
} from './errors';
describe('Errors', () => {
describe('ApolloError', () => {
const message = 'message';
it('defaults code to INTERNAL_SERVER_ERROR', () => {
const error = new ApolloError(message);
expect(error.message).to.equal(message);
expect(error.extensions.code).not.to.exist;
});
it('allows code setting and additional properties', () => {
const code = 'CODE';
const key = 'key';
const error = new ApolloError(message, code, { key });
expect(error.message).to.equal(message);
expect(error.key).to.equal(key);
expect(error.extensions.code).to.equal(code);
});
});
describe('formatApolloErrors', () => {
type CreateFormatError =
| ((options: Record<string, any>, errors) => Record<string, any>[])
| ((options?: Record<string, any>) => Record<string, any>);
const message = 'message';
const code = 'CODE';
const key = 'key';
const createFromttedError: CreateFormatError = (
options,
errors?: Error[],
) => {
if (errors === undefined) {
const error = new ApolloError(message, code, { key });
return formatApolloErrors(
[
new GraphQLError(
error.message,
undefined,
undefined,
undefined,
undefined,
error,
),
],
options,
)[0];
} else {
return formatApolloErrors(errors, options);
}
};
it('exposes a stacktrace in debug mode', () => {
const error = createFromttedError({ debug: true });
expect(error.message).to.equal(message);
expect(error.extensions.exception.key).to.equal(key);
expect(error.extensions.code).to.equal(code);
expect(
error.extensions.exception.stacktrace,
'stacktrace should exist under exception',
).to.exist;
});
it('hides stacktrace by default', () => {
const thrown = new Error(message);
(thrown as any).key = key;
const error = formatApolloErrors([
new GraphQLError(
thrown.message,
undefined,
undefined,
undefined,
undefined,
thrown,
),
])[0];
expect(error.message).to.equal(message);
expect(error.extensions.code).to.equal('INTERNAL_SERVER_ERROR');
expect(error.extensions.exception.key).to.equal(key);
expect(
error.extensions.exception.stacktrace,
'stacktrace should exist under exception',
).not.to.exist;
});
it('exposes fields on error under exception field and provides code', () => {
const error = createFromttedError();
expect(error.message).to.equal(message);
expect(error.extensions.exception.key).to.equal(key);
expect(error.extensions.code).to.equal(code);
expect(
error.extensions.exception.stacktrace,
'stacktrace should exist under exception',
).not.to.exist;
});
it('calls logFunction with each error', () => {
const error = new ApolloError(message, code, { key });
const logFunction = stub();
const formattedError = 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();
const formattedError = formatApolloErrors([error], {
formatter,
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(formatter.calledOnce);
});
});
describe('Named Errors', () => {
const message = 'message';
function verifyError(error, { code, errorClass, name }) {
expect(error.message).to.equal(message);
expect(error.extensions.code).to.equal(code);
expect(error.name).equals(name);
expect(error instanceof ApolloError).true;
expect(error instanceof errorClass).true;
}
it('provides an authentication error', () => {
verifyError(new AuthenticationError(message), {
code: 'UNAUTHENTICATED',
errorClass: AuthenticationError,
name: 'AuthenticationError',
});
});
it('provides a forbidden error', () => {
verifyError(new ForbiddenError(message), {
code: 'FORBIDDEN',
errorClass: ForbiddenError,
name: 'ForbiddenError',
});
});
it('provides a syntax error', () => {
verifyError(new SyntaxError(message), {
code: 'GRAPHQL_PARSE_FAILED',
errorClass: SyntaxError,
name: 'SyntaxError',
});
});
it('provides a validation error', () => {
verifyError(new ValidationError(message), {
code: 'GRAPHQL_VALIDATION_FAILED',
errorClass: ValidationError,
name: 'ValidationError',
});
});
});
});

View file

@ -0,0 +1,266 @@
import { GraphQLError } from 'graphql';
import { LogStep, LogAction, LogMessage, LogFunction } from './logging';
export class ApolloError extends Error implements GraphQLError {
public extensions: Record<string, any>;
readonly name;
readonly locations;
readonly path;
readonly source;
readonly positions;
readonly nodes;
public originalError;
[key: string]: any;
constructor(
message: string,
code?: string,
properties?: Record<string, any>,
) {
super(message);
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, ApolloError.prototype);
if (properties) {
Object.keys(properties).forEach(key => {
this[key] = properties[key];
});
}
//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
this.extensions = { code };
}
}
function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
const expanded = {} as any;
// follows similar structure to https://github.com/graphql/graphql-js/blob/master/src/error/GraphQLError.js#L145-L193
// with the addition of name
Object.defineProperties(expanded, {
name: {
value: error.name,
},
message: {
value: error.message,
enumerable: true,
writable: true,
},
locations: {
value: error.locations || undefined,
enumerable: true,
},
path: {
value: error.path || undefined,
enumerable: true,
},
nodes: {
value: error.nodes || undefined,
},
source: {
value: error.source || undefined,
},
positions: {
value: error.positions || undefined,
},
originalError: {
value: error.originalError,
},
});
expanded.extensions = {
...error.extensions,
code:
(error.extensions && error.extensions.code) || 'INTERNAL_SERVER_ERROR',
exception: {
...(error.extensions && error.extensions.exception),
...(error.originalError as any),
},
};
//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 =
(error.originalError &&
error.originalError.stack &&
error.originalError.stack.split('\n')) ||
(error.stack && error.stack.split('\n'));
}
if (Object.keys(expanded.extensions.exception).length === 0) {
//remove from printing an empty object
delete expanded.extensions.exception;
}
return expanded as ApolloError;
}
export function toApolloError(
error: Error & { extensions?: Record<string, any> },
code: string = 'INTERNAL_SERVER_ERROR',
): Error & { extensions: Record<string, any> } {
let err = error;
if (err.extensions) {
err.extensions.code = code;
} else {
err.extensions = { code };
}
return err as Error & { extensions: Record<string, any> };
}
export interface ErrorOptions {
code?: string;
errorClass?: typeof ApolloError;
}
export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
const copy: ApolloError =
options && options.errorClass
? new options.errorClass(error.message)
: new ApolloError(error.message);
//copy enumerable keys
Object.keys(error).forEach(key => {
copy[key] = error[key];
});
//extensions are non enumerable, so copy them directly
copy.extensions = {
...copy.extensions,
...error.extensions,
};
//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
Object.defineProperty(copy, 'originalError', { value: {} });
Object.getOwnPropertyNames(error).forEach(key => {
Object.defineProperty(copy.originalError, key, { value: error[key] });
});
return copy;
}
export class SyntaxError extends ApolloError {
constructor(message: string) {
super(message, 'GRAPHQL_PARSE_FAILED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, SyntaxError.prototype);
Object.defineProperty(this, 'name', { value: 'SyntaxError' });
}
}
export class ValidationError extends ApolloError {
constructor(message: string) {
super(message, 'GRAPHQL_VALIDATION_FAILED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, ValidationError.prototype);
Object.defineProperty(this, 'name', { value: 'ValidationError' });
}
}
export class AuthenticationError extends ApolloError {
constructor(message: string) {
super(message, 'UNAUTHENTICATED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, AuthenticationError.prototype);
Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
}
}
export class ForbiddenError extends ApolloError {
constructor(message: string) {
super(message, 'FORBIDDEN');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, ForbiddenError.prototype);
Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
}
}
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 flattenedErrors = [];
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
//
// They are are wrapped in an extra GraphQL error
// https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L109-L113
// which calls:
// https://github.com/graphql/graphql-js/blob/0a30b62964/src/error/locatedError.js#L18-L37
if (Array.isArray((error as any).errors)) {
(error as any).errors.forEach(e => flattenedErrors.push(e));
} else if (
(error as any).originalError &&
Array.isArray((error as any).originalError.errors)
) {
(error as any).originalError.errors.forEach(e => flattenedErrors.push(e));
} else {
flattenedErrors.push(error);
}
});
const enrichedErrors = flattenedErrors.map(error =>
enrichError(error, debug),
);
if (!formatter) {
return enrichedErrors;
}
return enrichedErrors.map(error => {
try {
return formatter(error);
} catch (err) {
logFunction({
action: LogAction.cleanup,
step: LogStep.status,
data: err,
key: 'error',
});
if (debug) {
return enrichError(err, debug);
} else {
//obscure error
const newError = fromGraphQLError(
new GraphQLError('Internal server error'),
);
return enrichError(newError, debug);
}
}
}) as Array<ApolloError>;
}

View file

@ -3,9 +3,8 @@ import {
ValidationContext,
GraphQLFieldResolver,
} from 'graphql';
import { LogFunction } from './runQuery';
import { LogFunction } from './logging';
import { GraphQLExtension } from 'graphql-extensions';
import { CacheControlExtensionOptions } from 'apollo-cache-control';
/*
* GraphQLServerOptions
@ -20,9 +19,14 @@ import { CacheControlExtensionOptions } from 'apollo-cache-control';
* - (optional) formatResponse: a function applied to each graphQL execution result
* - (optional) fieldResolver: a custom default field resolver
* - (optional) debug: a boolean that will print additional debug logging if execution errors occur
* - (optional) extensions: an array of functions which create GraphQLExtensions (each GraphQLExtension object is used for one request)
*
*/
export interface GraphQLServerOptions<TContext = any> {
export interface GraphQLServerOptions<
TContext =
| (() => Promise<Record<string, any>> | Record<string, any>)
| Record<string, any>
> {
schema: GraphQLSchema;
formatError?: Function;
rootValue?: any;
@ -34,21 +38,23 @@ export interface GraphQLServerOptions<TContext = any> {
fieldResolver?: GraphQLFieldResolver<any, TContext>;
debug?: boolean;
tracing?: boolean;
cacheControl?: boolean | CacheControlExtensionOptions;
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
extensions?: Array<() => GraphQLExtension>;
}
export default GraphQLServerOptions;
export async function resolveGraphqlOptions(
options: GraphQLServerOptions | Function,
...args
options:
| GraphQLServerOptions
| ((
...args: Array<any>
) => Promise<GraphQLServerOptions> | GraphQLServerOptions),
...args: Array<any>
): Promise<GraphQLServerOptions> {
if (typeof options === 'function') {
try {
return await options(...args);
} catch (e) {
throw new Error(`Invalid options provided to ApolloServer: ${e.message}`);
}
return await options(...args);
} else {
return options;
}

View file

@ -1,12 +1,22 @@
export {
runQuery,
LogFunction,
LogMessage,
LogStep,
LogAction,
} from './runQuery';
export { runQuery } from './runQuery';
export { LogFunction, LogMessage, LogStep, LogAction } from './logging';
export { runHttpQuery, HttpQueryRequest, HttpQueryError } from './runHttpQuery';
export {
default as GraphQLOptions,
resolveGraphqlOptions,
} from './graphqlOptions';
export {
ApolloError,
toApolloError,
SyntaxError,
ValidationError,
AuthenticationError,
ForbiddenError,
formatApolloErrors,
} from './errors';
export { convertNodeHttpToRequest } from './nodeHttpToRequest';
// ApolloServer Base class
export { ApolloServerBase } from './ApolloServer';
export * from './types';

View file

@ -0,0 +1,25 @@
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);
}

View file

@ -0,0 +1,19 @@
import { IncomingMessage } from 'http';
import { Request, Headers } from 'node-fetch';
export function convertNodeHttpToRequest(req: IncomingMessage): Request {
const headers = new Headers();
Object.keys(req.headers).forEach(key => {
const values = req.headers[key];
if (Array.isArray(values)) {
values.forEach(value => headers.append(key, value));
} else {
headers.append(key, values);
}
});
return new Request(req.url, {
headers,
method: req.method,
});
}

View file

@ -2,6 +2,7 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import 'mocha';
import * as MockReq from 'mock-req';
import {
GraphQLSchema,
@ -38,6 +39,7 @@ describe('runHttpQuery', () => {
options: {
schema,
},
request: new MockReq(),
};
it('raises a 400 error if the query is missing', () => {

View file

@ -1,20 +1,23 @@
import {
parse,
getOperationAST,
DocumentNode,
formatError,
ExecutionResult,
} from 'graphql';
import { runQuery } from './runQuery';
import { parse, DocumentNode, ExecutionResult } from 'graphql';
import { runQuery, QueryOptions } from './runQuery';
import {
default as GraphQLOptions,
resolveGraphqlOptions,
} from './graphqlOptions';
import { formatApolloErrors } from './errors';
export interface HttpQueryRequest {
method: string;
query: Record<string, any>;
options: GraphQLOptions | Function;
// query is either the POST body or the GET query string map. In the GET
// case, all values are strings and need to be parsed as JSON; in the POST
// case they should already be parsed. query has keys like 'query' (whose
// value should always be a string), 'variables', 'operationName',
// 'extensions', etc.
query: Record<string, any> | Array<Record<string, any>>;
options:
| GraphQLOptions
| ((...args: Array<any>) => Promise<GraphQLOptions> | GraphQLOptions);
request: Pick<Request, 'url' | 'method' | 'headers'>;
}
export class HttpQueryError extends Error {
@ -36,17 +39,14 @@ export class HttpQueryError extends Error {
}
}
function isQueryOperation(query: DocumentNode, operationName: string) {
const operationAST = getOperationAST(query, operationName);
return operationAST.operation === 'query';
}
export async function runHttpQuery(
handlerArguments: Array<any>,
request: HttpQueryRequest,
): Promise<string> {
let isGetRequest: boolean = false;
let optionsObject: GraphQLOptions;
const debugDefault =
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
try {
optionsObject = await resolveGraphqlOptions(
@ -54,9 +54,27 @@ export async function runHttpQuery(
...handlerArguments,
);
} catch (e) {
throw new HttpQueryError(500, e.message);
// 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
// as use NODE_ENV to determine the debug settings
e.message = `Invalid options provided to ApolloServer: ${e.message}`;
if (!debugDefault) {
e.warning = `To remove the stacktrace, set the NODE_ENV environment variable to production if the options creation can fail`;
}
throw new HttpQueryError(
500,
JSON.stringify({
errors: formatApolloErrors([e], { debug: debugDefault }),
}),
true,
{
'Content-Type': 'application/json',
},
);
}
const formatErrorFn = optionsObject.formatError || formatError;
const debug =
optionsObject.debug !== undefined ? optionsObject.debug : debugDefault;
let requestPayload;
switch (request.method) {
@ -98,9 +116,9 @@ export async function runHttpQuery(
requestPayload = [requestPayload];
}
const requests: Array<ExecutionResult> = requestPayload.map(requestParams => {
const requests = requestPayload.map(async requestParams => {
try {
let query = requestParams.query;
const queryString: string | undefined = requestParams.query;
let extensions = requestParams.extensions;
if (isGetRequest && extensions) {
@ -114,7 +132,11 @@ export async function runHttpQuery(
}
}
if (query === undefined && extensions && extensions.persistedQuery) {
if (
queryString === undefined &&
extensions &&
extensions.persistedQuery
) {
// It looks like we've received an Apollo Persisted Query. Apollo Server
// does not support persisted queries out of the box, so we should fail
// fast with a clear error saying that we don't support APQs. (A future
@ -137,31 +159,38 @@ export async function runHttpQuery(
);
}
if (isGetRequest) {
if (typeof query === 'string') {
// preparse the query incase of GET so we can assert the operation.
// XXX This makes the type of 'query' in this function confused
// which has led to us accidentally supporting GraphQL AST over
// the wire as a valid query, which confuses users. Refactor to
// not do this. Also, for a GET request, query really shouldn't
// ever be anything other than a string or undefined, so this
// set of conditionals doesn't quite make sense.
query = parse(query);
} else if (!query) {
// Note that we've already thrown a different error if it looks like APQ.
throw new HttpQueryError(400, 'Must provide query string.');
}
if (!isQueryOperation(query, requestParams.operationName)) {
//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') {
throw new HttpQueryError(
405,
`GET supports only query operation`,
false,
{
Allow: 'POST',
},
400,
"GraphQL queries must be strings. It looks like you're sending the " +
'internal graphql-js representation of a parsed query in your ' +
'request instead of a request in the GraphQL query language. You ' +
'can convert an AST to a string using the `print` function from ' +
'`graphql`, or use a client like `apollo-client` which converts ' +
'the internal representation to a string for you.',
);
}
throw new HttpQueryError(400, 'GraphQL queries must be strings.');
}
// GET operations should only be queries (not mutations). We want to throw
// a particular HTTP error in that case, but we don't actually parse the
// query until we're in runQuery, so we declare the error we want to throw
// here and pass it into runQuery.
// TODO this could/should be added as a validation rule rather than an ad hoc error
let nonQueryError;
if (isGetRequest) {
nonQueryError = new HttpQueryError(
405,
`GET supports only query operation`,
false,
{
Allow: 'POST',
},
);
}
const operationName = requestParams.operationName;
@ -178,9 +207,30 @@ export async function runHttpQuery(
}
}
let context = optionsObject.context || {};
if (typeof context === 'function') {
context = context();
let context = optionsObject.context;
if (!context) {
//appease typescript compiler, otherwise could use || {}
context = {};
} else if (typeof context === 'function') {
try {
context = await context();
} catch (e) {
e.message = `Context creation failed: ${e.message}`;
throw new HttpQueryError(
500,
JSON.stringify({
errors: formatApolloErrors([e], {
formatter: optionsObject.formatError,
debug,
logFunction: optionsObject.logFunction,
}),
}),
true,
{
'Content-Type': 'application/json',
},
);
}
} else if (isBatch) {
context = Object.assign(
Object.create(Object.getPrototypeOf(context)),
@ -188,42 +238,60 @@ export async function runHttpQuery(
);
}
let params = {
let params: QueryOptions = {
schema: optionsObject.schema,
query: query,
queryString,
nonQueryError,
variables: variables,
context,
rootValue: optionsObject.rootValue,
operationName: operationName,
logFunction: optionsObject.logFunction,
validationRules: optionsObject.validationRules,
formatError: formatErrorFn,
formatError: optionsObject.formatError,
formatResponse: optionsObject.formatResponse,
fieldResolver: optionsObject.fieldResolver,
debug: optionsObject.debug,
tracing: optionsObject.tracing,
cacheControl: optionsObject.cacheControl,
request: request.request,
extensions: optionsObject.extensions,
};
if (optionsObject.formatParams) {
params = optionsObject.formatParams(params);
}
if (!params.queryString && !params.parsedQuery) {
// Note that we've already thrown a different error if it looks like APQ.
throw new HttpQueryError(400, 'Must provide query string.');
}
return runQuery(params);
} catch (e) {
// Populate any HttpQueryError to our handler which should
// convert it to Http Error.
if (e.name === 'HttpQueryError') {
return Promise.reject(e);
//async function wraps this in a Promise
throw e;
}
return Promise.resolve({ errors: [formatErrorFn(e)] });
return {
errors: formatApolloErrors([e], {
formatter: optionsObject.formatError,
debug,
logFunction: optionsObject.logFunction,
}),
};
}
});
}) as Array<Promise<ExecutionResult>>;
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, JSON.stringify(gqlResponse), true, {
'Content-Type': 'application/json',

View file

@ -1,6 +1,7 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai';
import { stub } from 'sinon';
import * as MockReq from 'mock-req';
import 'mocha';
import {
@ -12,12 +13,14 @@ import {
parse,
} from 'graphql';
import { runQuery, LogAction, LogStep } from './runQuery';
import { runQuery } from './runQuery';
import { LogAction, LogStep } from './logging';
// Make the global Promise constructor Fiber-aware to simulate a Meteor
// environment.
import { makeCompatible } from 'meteor-promise';
import Fiber = require('fibers');
import { GraphQLExtensionStack, GraphQLExtension } from 'graphql-extensions';
makeCompatible(Promise, Fiber);
const queryType = new GraphQLObjectType({
@ -91,7 +94,11 @@ describe('runQuery', () => {
it('returns the right result when query is a string', () => {
const query = `{ testString }`;
const expected = { testString: 'it works' };
return runQuery({ schema, query: query }).then(res => {
return runQuery({
schema,
queryString: query,
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
});
});
@ -99,7 +106,11 @@ describe('runQuery', () => {
it('returns the right result when query is a document', () => {
const query = parse(`{ testString }`);
const expected = { testString: 'it works' };
return runQuery({ schema, query: query }).then(res => {
return runQuery({
schema,
parsedQuery: query,
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
});
});
@ -109,8 +120,9 @@ describe('runQuery', () => {
const expected = /Syntax Error/;
return runQuery({
schema,
query: query,
queryString: query,
variables: { base: 1 },
request: new MockReq(),
}).then(res => {
expect(res.data).to.be.undefined;
expect(res.errors.length).to.equal(1);
@ -118,28 +130,28 @@ describe('runQuery', () => {
});
});
it('sends stack trace to error if in an error occurs and debug mode is set', () => {
it('does not call console.error if in an error occurs and debug mode is set', () => {
const query = `query { testError }`;
const expected = /at resolveFieldValueOrError/;
const logStub = stub(console, 'error');
return runQuery({
schema,
query: query,
queryString: query,
debug: true,
request: new MockReq(),
}).then(res => {
logStub.restore();
expect(logStub.callCount).to.equal(1);
expect(logStub.getCall(0).args[0]).to.match(expected);
expect(logStub.callCount).to.equal(0);
});
});
it('does not send stack trace if in an error occurs and not in debug mode', () => {
it('does not call console.error if in an error occurs and not in debug mode', () => {
const query = `query { testError }`;
const logStub = stub(console, 'error');
return runQuery({
schema,
query: query,
queryString: query,
debug: false,
request: new MockReq(),
}).then(res => {
logStub.restore();
expect(logStub.callCount).to.equal(0);
@ -152,8 +164,9 @@ describe('runQuery', () => {
'Variable "$base" of type "String" used in position expecting type "Int!".';
return runQuery({
schema,
query: query,
queryString: query,
variables: { base: 1 },
request: new MockReq(),
}).then(res => {
expect(res.data).to.be.undefined;
expect(res.errors.length).to.equal(1);
@ -164,17 +177,25 @@ describe('runQuery', () => {
it('correctly passes in the rootValue', () => {
const query = `{ testRootValue }`;
const expected = { testRootValue: 'it also works' };
return runQuery({ schema, query: query, rootValue: 'it also' }).then(
res => {
expect(res.data).to.deep.equal(expected);
},
);
return runQuery({
schema,
queryString: query,
rootValue: 'it also',
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
});
});
it('correctly passes in the context', () => {
const query = `{ testContextValue }`;
const expected = { testContextValue: 'it still works' };
return runQuery({ schema, query: query, context: 'it still' }).then(res => {
return runQuery({
schema,
queryString: query,
context: 'it still',
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
});
});
@ -184,12 +205,13 @@ describe('runQuery', () => {
const expected = { testContextValue: 'it still works' };
return runQuery({
schema,
query: query,
queryString: query,
context: 'it still',
formatResponse: (response, { context }) => {
response['extensions'] = context;
return response;
},
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
expect(res['extensions']).to.equal('it still');
@ -201,8 +223,9 @@ describe('runQuery', () => {
const expected = { testArgumentValue: 6 };
return runQuery({
schema,
query: query,
queryString: query,
variables: { base: 1 },
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
});
@ -214,7 +237,8 @@ describe('runQuery', () => {
'Variable "$base" of required type "Int!" was not provided.';
return runQuery({
schema,
query: query,
queryString: query,
request: new MockReq(),
}).then(res => {
expect(res.errors[0].message).to.deep.equal(expected);
});
@ -223,7 +247,8 @@ describe('runQuery', () => {
it('supports yielding resolver functions', () => {
return runQuery({
schema,
query: `{ testAwaitedValue }`,
queryString: `{ testAwaitedValue }`,
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal({
testAwaitedValue: 'it works',
@ -242,7 +267,12 @@ describe('runQuery', () => {
const expected = {
testString: 'it works',
};
return runQuery({ schema, query: query, operationName: 'Q1' }).then(res => {
return runQuery({
schema,
queryString: query,
operationName: 'Q1',
request: new MockReq(),
}).then(res => {
expect(res.data).to.deep.equal(expected);
});
});
@ -259,10 +289,11 @@ describe('runQuery', () => {
};
return runQuery({
schema,
query: query,
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);
@ -291,6 +322,10 @@ describe('runQuery', () => {
expect(logs[10]).to.deep.equals({
action: LogAction.request,
step: LogStep.end,
key: 'response',
data: {
data: expected,
},
});
});
});
@ -306,8 +341,9 @@ describe('runQuery', () => {
const result1 = await runQuery({
schema,
query: query,
queryString: query,
operationName: 'Q1',
request: new MockReq(),
});
expect(result1.data).to.deep.equal({
@ -318,9 +354,10 @@ describe('runQuery', () => {
const result2 = await runQuery({
schema,
query: query,
queryString: query,
operationName: 'Q1',
fieldResolver: () => 'a very testful field resolver string',
request: new MockReq(),
});
expect(result2.data).to.deep.equal({
@ -330,6 +367,59 @@ describe('runQuery', () => {
});
});
describe('graphql extensions', () => {
class CustomExtension implements GraphQLExtension<any> {
format(): [string, any] {
return ['customExtension', { foo: 'bar' }];
}
}
it('creates the extension stack', async () => {
const queryString = `{ testString }`;
const expected = { testString: 'it works' };
const extensions = [() => new CustomExtension()];
return runQuery({
schema: new GraphQLSchema({
query: new GraphQLObjectType({
name: 'QueryType',
fields: {
testString: {
type: GraphQLString,
resolve(root, args, context) {
expect(context._extensionStack).to.be.instanceof(
GraphQLExtensionStack,
);
expect(
context._extensionStack.extensions[0],
).to.be.instanceof(CustomExtension);
},
},
},
}),
}),
queryString,
extensions,
request: new MockReq(),
});
});
it('runs format response from extensions', async () => {
const queryString = `{ testString }`;
const expected = { testString: 'it works' };
const extensions = [() => new CustomExtension()];
return runQuery({
schema,
queryString,
extensions,
request: new MockReq(),
}).then(res => {
return expect(res.extensions).to.deep.equal({
customExtension: { foo: 'bar' },
});
});
});
});
describe('async_hooks', () => {
let asyncHooks;
let asyncHook;
@ -361,8 +451,9 @@ describe('runQuery', () => {
await runQuery({
schema,
query: query,
queryString: query,
operationName: 'Q1',
request: new MockReq(),
});
// this is the only async process so we expect the async ids to be a sequence

View file

@ -7,8 +7,9 @@ import {
print,
validate,
execute,
ExecutionArgs,
getOperationAST,
GraphQLError,
formatError,
specifiedRules,
ValidationContext,
} from 'graphql';
@ -17,12 +18,20 @@ import {
enableGraphQLExtensions,
GraphQLExtension,
GraphQLExtensionStack,
EndHandler,
} from 'graphql-extensions';
import { TracingExtension } from 'apollo-tracing';
import { CacheControlExtension } from 'apollo-cache-control';
import {
CacheControlExtension,
CacheControlExtensionOptions,
} from 'apollo-cache-control';
fromGraphQLError,
formatApolloErrors,
ValidationError,
SyntaxError,
} from './errors';
import { LogStep, LogAction, LogMessage, LogFunction } from './logging';
import { GraphQLRequest } from 'apollo-fetch';
export interface GraphQLResponse {
data?: object;
@ -30,33 +39,17 @@ export interface GraphQLResponse {
extensions?: object;
}
export enum LogAction {
request,
parse,
validation,
execute,
}
export enum LogStep {
start,
end,
status,
}
export interface LogMessage {
action: LogAction;
step: LogStep;
key?: string;
data?: Object;
}
export interface LogFunction {
(message: LogMessage);
}
export interface QueryOptions {
schema: GraphQLSchema;
query: string | DocumentNode;
// Specify exactly one of these. parsedQuery is primarily for use by
// OperationStore.
queryString?: string;
parsedQuery?: DocumentNode;
// If this is specified and the given GraphQL query is not a "query" (eg, it's
// a mutation), throw this error.
nonQueryError?: Error;
rootValue?: any;
context?: any;
variables?: { [key: string]: any };
@ -71,7 +64,15 @@ export interface QueryOptions {
formatResponse?: Function;
debug?: boolean;
tracing?: boolean;
cacheControl?: boolean | CacheControlExtensionOptions;
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
request: Pick<Request, 'url' | 'method' | 'headers'>;
extensions?: Array<() => GraphQLExtension>;
}
function isQueryOperation(query: DocumentNode, operationName: string) {
const operationAST = getOperationAST(query, operationName);
return operationAST.operation === 'query';
}
export function runQuery(options: QueryOptions): Promise<GraphQLResponse> {
@ -79,28 +80,13 @@ export function runQuery(options: QueryOptions): Promise<GraphQLResponse> {
return Promise.resolve().then(() => doRunQuery(options));
}
function printStackTrace(error: Error) {
console.error(error.stack);
}
function format(errors: Array<Error>, formatter?: Function): Array<Error> {
return errors.map(error => {
if (formatter !== undefined) {
try {
return formatter(error);
} catch (err) {
console.error('Error in formatError function:', err);
const newError = new Error('Internal server error');
return formatError(newError);
}
} else {
return formatError(error);
}
}) as Array<Error>;
}
function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
let documentAST: DocumentNode;
if (options.queryString && options.parsedQuery) {
throw new Error('Only supply one of queryString and parsedQuery');
}
if (!(options.queryString || options.parsedQuery)) {
throw new Error('Must supply one of queryString and parsedQuery');
}
const logFunction =
options.logFunction ||
@ -114,124 +100,216 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
logFunction({ action: LogAction.request, step: LogStep.start });
const context = options.context || {};
let extensions = [];
// If custom extension factories were provided, create per-request extension objects.
const extensions = options.extensions ? options.extensions.map(f => f()) : [];
// Legacy hard-coded extension factories. The ApolloServer class doesn't use
// this code path, but older APIs did.
if (options.tracing) {
extensions.push(TracingExtension);
extensions.push(new TracingExtension());
}
if (options.cacheControl === true) {
extensions.push(CacheControlExtension);
extensions.push(new CacheControlExtension());
} else if (options.cacheControl) {
extensions.push(new CacheControlExtension(options.cacheControl));
}
const extensionStack =
extensions.length > 0 && new GraphQLExtensionStack(extensions);
if (extensionStack) {
const extensionStack = new GraphQLExtensionStack(extensions);
// We unconditionally create an extensionStack (so that we don't have to
// litter the rest of this function with `if (extensionStack)`, but we don't
// instrument the schema unless there actually are extensions.
if (extensions.length > 0) {
context._extensionStack = extensionStack;
enableGraphQLExtensions(options.schema);
extensionStack.requestDidStart();
}
const qry =
typeof options.query === 'string' ? options.query : print(options.query);
logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'query',
data: qry,
const requestDidEnd = extensionStack.requestDidStart({
// Since the Request interfacess are not the same between node-fetch and
// typescript's lib dom, we should limit the fields that need to be passed
// into requestDidStart to only the ones we need, currently just the
// headers, method, and url
request: options.request as any,
});
logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'variables',
data: options.variables,
});
logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'operationName',
data: options.operationName,
});
// if query is already an AST, don't parse or validate
// XXX: This refers the operations-store flow.
if (typeof options.query === 'string') {
try {
logFunction({ action: LogAction.parse, step: LogStep.start });
documentAST = parse(options.query as string);
logFunction({ action: LogAction.parse, step: LogStep.end });
} catch (syntaxError) {
logFunction({ action: LogAction.parse, step: LogStep.end });
return Promise.resolve({
errors: format([syntaxError], options.formatError),
return Promise.resolve()
.then(() => {
const loggedQuery = options.queryString || print(options.parsedQuery);
logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'query',
data: loggedQuery,
});
logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'variables',
data: options.variables,
});
logFunction({
action: LogAction.request,
step: LogStep.status,
key: 'operationName',
data: options.operationName,
});
}
} else {
documentAST = options.query as DocumentNode;
}
let rules = specifiedRules;
if (options.validationRules) {
rules = rules.concat(options.validationRules);
}
logFunction({ action: LogAction.validation, step: LogStep.start });
const validationErrors = validate(options.schema, documentAST, rules);
logFunction({ action: LogAction.validation, step: LogStep.end });
if (validationErrors.length) {
return Promise.resolve({
errors: format(validationErrors, options.formatError),
});
}
if (extensionStack) {
extensionStack.executionDidStart();
}
try {
logFunction({ action: LogAction.execute, step: LogStep.start });
return Promise.resolve(
execute(
options.schema,
documentAST,
options.rootValue,
context,
options.variables,
options.operationName,
options.fieldResolver,
),
).then(result => {
logFunction({ action: LogAction.execute, step: LogStep.end });
logFunction({ action: LogAction.request, step: LogStep.end });
let response: GraphQLResponse = {
data: result.data,
};
if (result.errors) {
response.errors = format(result.errors, options.formatError);
if (debug) {
result.errors.map(printStackTrace);
// Parse the document.
let documentAST: DocumentNode;
if (options.parsedQuery) {
documentAST = options.parsedQuery;
} else if (!options.queryString) {
throw new Error('Must supply one of queryString and parsedQuery');
} else {
logFunction({ action: LogAction.parse, step: LogStep.start });
const parsingDidEnd = extensionStack.parsingDidStart({
queryString: options.queryString,
});
let graphqlParseErrors;
try {
documentAST = parse(options.queryString);
} catch (syntaxError) {
graphqlParseErrors = formatApolloErrors(
[
fromGraphQLError(syntaxError, {
errorClass: SyntaxError,
}),
],
{
formatter: options.formatError,
debug,
},
);
} finally {
parsingDidEnd(...(graphqlParseErrors || []));
logFunction({ action: LogAction.parse, step: LogStep.end });
if (graphqlParseErrors) {
return Promise.resolve({ errors: graphqlParseErrors });
}
}
}
if (extensionStack) {
extensionStack.executionDidEnd();
extensionStack.requestDidEnd();
response.extensions = extensionStack.format();
if (
options.nonQueryError &&
!isQueryOperation(documentAST, options.operationName)
) {
// XXX this goes to requestDidEnd, is that correct or should it be
// validation?
throw options.nonQueryError;
}
if (options.formatResponse) {
response = options.formatResponse(response, options);
let rules = specifiedRules;
if (options.validationRules) {
rules = rules.concat(options.validationRules);
}
logFunction({ action: LogAction.validation, step: LogStep.start });
const validationDidEnd = extensionStack.validationDidStart();
let validationErrors;
try {
validationErrors = validate(options.schema, documentAST, rules);
} catch (validationThrewError) {
// Catch errors thrown by validate, not just those returned by it.
validationErrors = [validationThrewError];
} finally {
try {
if (validationErrors) {
validationErrors = formatApolloErrors(
validationErrors.map(err =>
fromGraphQLError(err, { errorClass: ValidationError }),
),
{
formatter: options.formatError,
logFunction,
debug,
},
);
}
} finally {
validationDidEnd(...(validationErrors || []));
logFunction({ action: LogAction.validation, step: LogStep.end });
if (validationErrors && validationErrors.length) {
return Promise.resolve({
errors: validationErrors,
});
}
}
}
return response;
const executionArgs: ExecutionArgs = {
schema: options.schema,
document: documentAST,
rootValue: options.rootValue,
contextValue: context,
variableValues: options.variables,
operationName: options.operationName,
fieldResolver: options.fieldResolver,
};
logFunction({ action: LogAction.execute, step: LogStep.start });
const executionDidEnd = extensionStack.executionDidStart({
executionArgs,
});
return Promise.resolve()
.then(() => execute(executionArgs))
.catch(executionError => {
return {
// These errors will get passed through formatApolloErrors in the
// `then` below.
// TODO accurate code for this error, which describes this error, which
// can occur when:
// * variables incorrectly typed/null when nonnullable
// * unknown operation/operation name invalid
// * operation type is unsupported
// Options: PREPROCESSING_FAILED, GRAPHQL_RUNTIME_CHECK_FAILED
errors: [fromGraphQLError(executionError)],
} as ExecutionResult;
})
.then(result => {
let response: GraphQLResponse = {
data: result.data,
};
if (result.errors) {
response.errors = formatApolloErrors([...result.errors], {
formatter: options.formatError,
logFunction,
debug,
});
}
executionDidEnd(...result.errors);
logFunction({ action: LogAction.execute, step: LogStep.end });
const formattedExtensions = extensionStack.format();
if (Object.keys(formattedExtensions).length > 0) {
response.extensions = formattedExtensions;
}
if (options.formatResponse) {
response = options.formatResponse(response, options);
}
return response;
});
})
.catch(err => {
// Handle the case of an internal server failure (or nonQueryError) ---
// we're not returning a GraphQL response so we don't call
// willSendResponse.
requestDidEnd(err);
logFunction({ action: LogAction.request, step: LogStep.end });
throw err;
})
.then(graphqlResponse => {
extensionStack.willSendResponse({ graphqlResponse });
requestDidEnd();
logFunction({
action: LogAction.request,
step: LogStep.end,
key: 'response',
data: graphqlResponse,
});
return graphqlResponse;
});
} catch (executionError) {
logFunction({ action: LogAction.execute, step: LogStep.end });
logFunction({ action: LogAction.request, step: LogStep.end });
return Promise.resolve({
errors: format([executionError], options.formatError),
});
}
}

View file

@ -0,0 +1,90 @@
import { GraphQLSchema } from 'graphql';
import { SchemaDirectiveVisitor, IResolvers, IMocks } from 'graphql-tools';
import { ConnectionContext } from 'subscriptions-transport-ws';
import { Server as HttpServer } from 'http';
import { ListenOptions as HttpListenOptions } from 'net';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingOptions } from 'apollo-engine-reporting';
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions';
export type Context<T = any> = T;
export type ContextFunction<T = any> = (
context: Context<T>,
) => Promise<Context<T>>;
export interface SubscriptionServerOptions {
path: string;
keepAlive?: number;
onConnect?: (
connectionParams: Object,
websocket: WebSocket,
context: ConnectionContext,
) => any;
onDisconnect?: (websocket: WebSocket, context: ConnectionContext) => any;
}
export interface Config<Server>
extends Pick<
GraphQLOptions<Context<any>>,
| 'formatError'
| 'debug'
| 'rootValue'
| 'logFunction'
| 'formatParams'
| 'validationRules'
| 'formatResponse'
| 'fieldResolver'
| 'debug'
| 'cacheControl'
| 'tracing'
> {
typeDefs?: string | [string];
resolvers?: IResolvers;
schema?: GraphQLSchema;
schemaDirectives?: Record<string, typeof SchemaDirectiveVisitor>;
context?: Context<any> | ContextFunction<any>;
introspection?: boolean;
mocks?: boolean | IMocks;
engine?: boolean | EngineReportingOptions;
extensions?: Array<() => GraphQLExtension>;
}
// XXX export these directly from apollo-engine-js
export interface EngineLauncherOptions {
startupTimeout?: number;
proxyStdoutStream?: NodeJS.WritableStream;
proxyStderrStream?: NodeJS.WritableStream;
extraArgs?: string[];
processCleanupEvents?: string[];
}
export interface ListenOptions {
// node http listen options
// https://nodejs.org/api/net.html#net_server_listen_options_callback
// https://github.com/apollographql/apollo-server/pull/979#discussion_r184483094
http?: HttpListenOptions | any | { handle: any; backlog?: number };
// XXX clean this up
engineInRequestPath?: boolean;
engineProxy?: boolean | Record<string, any>;
// engine launcher options
engineLauncherOptions?: EngineLauncherOptions;
// WebSocket options
subscriptions?: Partial<SubscriptionServerOptions> | string | false;
}
export interface MiddlewareOptions {
path?: string;
gui?: boolean;
subscriptions?: boolean;
}
export interface RegistrationOptions {
path: string;
getHttp: () => HttpServer;
}
export interface ServerInfo {
url: string;
port: number | string;
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-express",
"version": "1.3.6",
"version": "2.0.0-beta.2",
"description": "Production-ready Node.js GraphQL server for Express and Connect",
"main": "dist/index.js",
"scripts": {
@ -19,28 +19,36 @@
"Connect",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"author": "opensource@apollographql.com",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-module-graphiql": "^1.3.4"
"@types/accepts": "^1.3.5",
"accepts": "^1.3.5",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4",
"apollo-upload-server": "^5.0.0",
"body-parser": "^1.18.3",
"cors": "^2.8.4",
"graphql-playground-middleware-express": "^1.6.2"
},
"devDependencies": {
"@types/body-parser": "1.17.0",
"@types/connect": "3.4.32",
"@types/cors": "^2.8.4",
"@types/express": "4.11.1",
"@types/graphql": "0.12.7",
"@types/multer": "1.3.6",
"@types/node": "^10.1.2",
"apollo-server-integration-testsuite": "^1.3.6",
"body-parser": "1.18.2",
"connect": "3.6.6",
"connect-query": "1.0.0",
"express": "4.16.3",
"multer": "1.3.0"
"form-data": "^2.3.2",
"multer": "1.3.0",
"node-fetch": "^2.1.2"
},
"typings": "dist/index.d.ts",
"typescript": {

View file

@ -0,0 +1,499 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import 'mocha';
import * as express from 'express';
import * as request from 'request';
import * as FormData from 'form-data';
import * as fs from 'fs';
import fetch from 'node-fetch';
import { createApolloFetch } from 'apollo-fetch';
import { ApolloServerBase, AuthenticationError } from 'apollo-server-core';
import { registerServer } from './ApolloServer';
const gql = String.raw;
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hi',
},
};
describe('apollo-server-express', () => {
//to remove the circular dependency, we reference it directly
const ApolloServer = require('../../apollo-server/dist/index').ApolloServer;
describe('', () => {
it('accepts typeDefs and resolvers', () => {
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
expect(() => registerServer({ app, server })).not.to.throw;
});
it('accepts typeDefs and mocks', () => {
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
expect(() => registerServer({ app, server })).not.to.throw;
});
});
describe('registerServer', () => {
let server: ApolloServerBase<express.Request>;
let app: express.Application;
afterEach(async () => {
await server.stop();
});
it('can be queried', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
registerServer({ app, server });
const { url: uri } = await server.listen();
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;
});
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 = express();
registerServer({ app, server });
const { url } = await server.listen();
return new Promise((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();
}
},
);
});
});
it('accepts cors configuration', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
registerServer({ app, server, cors: { origin: 'apollographql.com' } });
const { url: uri } = await server.listen({});
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}' });
});
it('accepts body parser configuration', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
registerServer({ app, server, bodyParserConfig: { limit: 0 } });
const { url: uri } = await server.listen({});
const apolloFetch = createApolloFetch({ uri });
return new Promise((resolve, reject) => {
apolloFetch({ query: '{hello}' })
.then(reject)
.catch(error => {
expect(error.response).to.exist;
expect(error.response.status).to.equal(413);
expect(error.toString()).to.contain('Payload Too Large');
resolve();
});
});
});
describe('healthchecks', () => {
let server: ApolloServerBase<express.Request>;
afterEach(async () => {
await server.stop();
});
it('creates a healthcheck endpoint', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
app = express();
registerServer({ app, server, bodyParserConfig: { limit: 0 } });
const { port } = await server.listen();
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 = express();
registerServer({
app,
server,
onHealthCheck: async () => {
throw Error("can't connect to DB");
},
});
const { port } = await server.listen({});
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 = express();
registerServer({
app,
server,
disableHealthCheck: true,
});
const { port } = await server.listen({});
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(response.statusCode).to.equal(404);
resolve();
}
},
);
});
});
});
describe('file uploads', () => {
it('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: (parent, args) => {},
},
Mutation: {
singleUpload: async (parent, args) => {
expect((await args.file).stream).to.exist;
return args.file;
},
},
},
});
app = express();
registerServer({
app,
server,
});
const { port } = await server.listen({});
const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: gql`
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,
});
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: (parent, args, context) => {
throw Error('never get here');
},
},
};
server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
throw new AuthenticationError('valid result');
},
});
app = express();
registerServer({ app, server });
const { url: uri } = await server.listen();
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;
await server.stop();
});
it('propogates error codes in dev mode', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const server = new ApolloServer({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
app = express();
registerServer({ app, server });
const { url: uri } = await server.listen();
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;
await server.stop();
});
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 = express();
registerServer({ app, server });
const { url: uri } = await server.listen();
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;
await server.stop();
});
it('propogates error codes with null response in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const server = new ApolloServer({
typeDefs: gql`
type Query {
error: String!
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
app = express();
registerServer({ app, server });
const { url: uri } = await server.listen();
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;
await server.stop();
});
});
});
});

View file

@ -0,0 +1,143 @@
import * as express from 'express';
import * as corsMiddleware from 'cors';
import { json, OptionsJson } from 'body-parser';
import { createServer, Server as HttpServer } from 'http';
import gui from 'graphql-playground-middleware-express';
import { ApolloServerBase, formatApolloErrors } from 'apollo-server-core';
import * as accepts from 'accepts';
import { graphqlExpress } from './expressApollo';
import {
processRequest as processFileUploads,
GraphQLUpload,
} from 'apollo-upload-server';
const gql = String.raw;
export interface ServerRegistration {
app: express.Application;
server: ApolloServerBase<express.Request>;
path?: string;
cors?: corsMiddleware.CorsOptions;
bodyParserConfig?: OptionsJson;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
//https://github.com/jaydenseric/apollo-upload-server#options
uploads?: boolean | Record<string, any>;
}
const fileUploadMiddleware = (
uploadsConfig: Record<string, any>,
server: ApolloServerBase<express.Request>,
) => (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
if (req.is('multipart/form-data')) {
processFileUploads(req, uploadsConfig)
.then(body => {
req.body = body;
next();
})
.catch(error => {
if (error.status && error.expose) res.status(error.status);
next(
formatApolloErrors([error], {
formatter: server.requestOptions.formatError,
debug: server.requestOptions.debug,
logFunction: server.requestOptions.logFunction,
}),
);
});
} else {
next();
}
};
export const registerServer = async ({
app,
server,
path,
cors,
bodyParserConfig,
disableHealthCheck,
onHealthCheck,
uploads,
}: ServerRegistration) => {
if (!path) path = '/graphql';
if (!disableHealthCheck) {
//uses same path as engine
app.use('/.well-known/apollo/server-health', (req, res, next) => {
//Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');
if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
});
}
let uploadsMiddleware;
if (uploads !== false) {
server.enhanceSchema({
typeDefs: gql`
scalar Upload
`,
resolvers: { Upload: GraphQLUpload },
});
uploadsMiddleware = fileUploadMiddleware(
typeof uploads !== 'boolean' ? uploads : {},
server,
);
}
// XXX multiple paths?
server.use({
path,
getHttp: () => createServer(app),
});
app.use(
path,
corsMiddleware(cors),
json(bodyParserConfig),
uploadsMiddleware ? uploadsMiddleware : (req, res, next) => next(),
(req, res, next) => {
// make sure we check to see if graphql gui should be on
if (!server.disableTools && req.method === 'GET') {
//perform more expensive content-type check only if necessary
const accept = accepts(req);
const types = accept.types() as string[];
const prefersHTML =
types.find(
(x: string) => x === 'text/html' || x === 'application/json',
) === 'text/html';
if (prefersHTML) {
return gui({
endpoint: path,
subscriptionEndpoint: server.subscriptionsPath,
})(req, res, next);
}
}
return graphqlExpress(server.graphQLServerOptionsForRequest.bind(server))(
req,
res,
next,
);
},
);
};

View file

@ -324,12 +324,14 @@ describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
query: '{thrower}',
});
// console.log(response.text);
expect(response.status).to.equal(200);
expect(JSON.parse(response.text)).to.deep.equal({
data: null,
errors: [
{
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
message: 'Throws!',
locations: [{ line: 1, column: 2 }],
path: ['thrower'],
@ -359,6 +361,9 @@ describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
expect(JSON.parse(response.text)).to.deep.equal({
errors: [
{
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
message: 'Cannot query field "notExists" on type "QueryRoot".',
locations: [{ line: 1, column: 2 }],
},
@ -385,6 +390,9 @@ describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
expect(JSON.parse(response.text)).to.deep.equal({
errors: [
{
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
message: 'Cannot query field "notExists" on type "QueryRoot".',
locations: [{ line: 1, column: 2 }],
},
@ -532,6 +540,9 @@ describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
expect(JSON.parse(response.text)).to.deep.equal({
errors: [
{
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
message: 'AlwaysInvalidRule was really invalid!',
},
],

View file

@ -4,6 +4,7 @@ import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
convertNodeHttpToRequest,
} from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
@ -45,6 +46,7 @@ export function graphqlExpress(
method: req.method,
options: options,
query: req.method === 'POST' ? req.body : req.query,
request: convertNodeHttpToRequest(req),
}).then(
gqlResponse => {
res.setHeader('Content-Type', 'application/json');

View file

@ -1,5 +1,13 @@
// Expose types which can be used by both middleware flavors.
export { GraphQLOptions } from 'apollo-server-core';
export {
ApolloError,
toApolloError,
SyntaxError,
ValidationError,
AuthenticationError,
ForbiddenError,
} from 'apollo-server-core';
// Express Middleware
export {
@ -12,3 +20,6 @@ export {
// Connect Middleware
export { graphqlConnect, graphiqlConnect } from './connectApollo';
// ApolloServer integration
export { registerServer } from './ApolloServer';

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -8,51 +8,79 @@ 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 all Node.js HTTP server frameworks: Express, Connect, Hapi, Koa and Restify. [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
npm install apollo-server@beta apollo-server-hapi@beta
```
## Usage
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.
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
import Hapi from 'hapi';
import { graphqlHapi } from 'apollo-server-hapi';
const { ApolloServer, gql } = require('apollo-server');
const { registerServer } = require('apollo-server-hapi');
const HOST = 'localhost';
const PORT = 3000;
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hello',
},
}
async function StartServer() {
const server = new Hapi.server({
host: HOST,
port: PORT,
});
const server = new ApolloServer({ typeDefs, resolvers });
await server.register({
plugin: graphqlHapi,
await registerServer({
server,
//Hapi Server constructor options
options: {
path: '/graphql',
graphqlOptions: {
schema: myGraphQLSchema,
},
route: {
cors: true,
},
host: HOST,
},
});
try {
await server.start();
} catch (err) {
console.log(`Error while starting server: ${err.message}`);
}
console.log(`Server running at: ${server.info.uri}`);
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
}
StartServer();
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 } = require('apollo-server');
const { registerServer } = 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,
});
await registerServer({
server,
app,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
}
StartServer().catch(error => console.log(error));
```
## Principles

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-hapi",
"version": "1.3.6",
"version": "2.0.0-beta.1",
"description": "Production-ready Node.js GraphQL server for Hapi",
"main": "dist/index.js",
"scripts": {
@ -25,14 +25,17 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"accept": "^3.0.2",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4",
"boom": "^7.1.0"
"apollo-upload-server": "^5.0.0",
"boom": "^7.1.0",
"graphql-playground-html": "^1.5.6"
},
"devDependencies": {
"@types/graphql": "0.12.7",
"@types/hapi": "^17.0.12",
"apollo-server-integration-testsuite": "^1.3.6",
"hapi": "17.3.1"
"hapi": "17.4.0"
},
"typings": "dist/index.d.ts",
"typescript": {

View file

@ -0,0 +1,199 @@
import * as hapi from 'hapi';
import { createServer, Server as HttpServer } from 'http';
import { ApolloServerBase, EngineLauncherOptions } from 'apollo-server-core';
import { parseAll } from 'accept';
import { renderPlaygroundPage } from 'graphql-playground-html';
import {
processRequest as processFileUploads,
GraphQLUpload,
} from 'apollo-upload-server';
import { graphqlHapi } from './hapiApollo';
const gql = String.raw;
export interface ServerRegistration {
app?: hapi.Server;
//The options type should exclude port
options?: hapi.ServerOptions;
server: ApolloServerBase<hapi.Request>;
path?: string;
cors?: boolean;
onHealthCheck?: (req: hapi.Request) => Promise<any>;
disableHealthCheck?: boolean;
uploads?: boolean | Record<string, any>;
}
export interface HapiListenOptions {
port?: number | string;
host?: string; // default: ''. This is where engineproxy listens.
pipePath?: string;
graphqlPaths?: string[]; // default: ['/graphql']
innerHost?: string; // default: '127.0.0.1'. This is where Node listens.
launcherOptions?: EngineLauncherOptions;
}
const handleFileUploads = (
uploadsConfig: Record<string, any>,
server: ApolloServerBase<hapi.Request>,
) => async (req: hapi.Request, h: hapi.ResponseToolkit) => {
if (req.mime === 'multipart/form-data') {
Object.defineProperty(req, 'payload', {
value: await processFileUploads(req, uploadsConfig),
writable: false,
});
}
};
export const registerServer = async ({
app,
options,
server,
cors,
path,
disableHealthCheck,
onHealthCheck,
uploads,
}: ServerRegistration) => {
if (!path) path = '/graphql';
let hapiApp: hapi.Server;
if (app) {
hapiApp = app;
if (options) {
console.warn(`A Hapi Server was passed in, so the options are ignored`);
}
} else if (options) {
if ((options as any).port) {
throw new Error(`
The options for registerServer should not include a port, since autoListen is set to false. Please set the port under the http options in listen:
const server = new ApolloServer({ typeDefs, resolvers });
registerServer({
server,
options,
});
server.listen({ http: { port: YOUR_PORT_HERE } });
`);
}
hapiApp = new hapi.Server({ ...options, autoListen: false });
} else {
hapiApp = new hapi.Server({ autoListen: false });
}
if (uploads !== false) {
server.enhanceSchema({
typeDefs: gql`
scalar Upload
`,
resolvers: { Upload: GraphQLUpload },
});
}
await hapiApp.ext({
type: 'onRequest',
method: async function(request, h) {
if (request.path !== path) {
return h.continue;
}
if (uploads !== false) {
await handleFileUploads(
typeof uploads !== 'boolean' ? uploads : {},
server,
)(request, h);
}
if (!server.disableTools && request.method === 'get') {
//perform more expensive content-type check only if necessary
const accept = parseAll(request.headers);
const types = accept.mediaTypes as string[];
const prefersHTML =
types.find(
(x: string) => x === 'text/html' || x === 'application/json',
) === 'text/html';
if (prefersHTML) {
return h
.response(
renderPlaygroundPage({
subscriptionEndpoint: server.subscriptionsPath,
endpoint: path,
version: '1.4.0',
}),
)
.type('text/html')
.takeover();
}
}
return h.continue;
},
});
if (!disableHealthCheck) {
await hapiApp.route({
method: '*',
path: '/.well-known/apollo/server-health',
options: {
cors: typeof cors === 'boolean' ? cors : true,
},
handler: async function(request, h) {
if (onHealthCheck) {
try {
await onHealthCheck(request);
} catch {
const response = h.response({ status: 'fail' });
response.code(503);
response.type('application/health+json');
return response;
}
}
const response = h.response({ status: 'pass' });
response.type('application/health+json');
return response;
},
});
}
await hapiApp.register({
plugin: graphqlHapi,
options: {
path: path,
graphqlOptions: server.graphQLServerOptionsForRequest.bind(server),
route: {
cors: typeof cors === 'boolean' ? cors : true,
},
},
});
server.use({ path, getHttp: () => hapiApp.listener });
const listen = server.listen.bind(server);
server.listen = async options => {
//requires that autoListen is false, so that
//hapi sets up app.listener without start
await hapiApp.start();
//While this is not strictly necessary, it ensures that apollo server calls
//listen first, setting the port. Otherwise the hapi server constructor
//sets the port
if (hapiApp.listener.listening) {
throw Error(
`
Ensure that constructor of Hapi server sets autoListen to false, as follows:
const app = Hapi.server({
autoListen: false,
//other parameters
});
`,
);
}
//starts the hapi listener at a random port when engine proxy used,
//otherwise will start the server at the provided port
return listen({ ...options });
};
};

View file

@ -1,11 +1,13 @@
import * as Boom from 'boom';
import { Server, Response, Request, ReplyNoContinue } from 'hapi';
import { Server, Request } from 'hapi';
import * as GraphiQL from 'apollo-server-module-graphiql';
import {
GraphQLOptions,
runHttpQuery,
HttpQueryError,
convertNodeHttpToRequest,
} from 'apollo-server-core';
import { IncomingMessage } from 'http';
export interface IRegister {
(server: Server, options: any): void;
@ -39,13 +41,18 @@ const graphqlHapi: IPlugin = {
method: ['GET', 'POST'],
path: options.path || '/graphql',
vhost: options.vhost || undefined,
config: options.route || {},
options: options.route || {},
handler: async (request, h) => {
try {
const gqlResponse = await runHttpQuery([request], {
method: request.method.toUpperCase(),
options: options.graphqlOptions,
query: request.method === 'post' ? request.payload : request.query,
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);
@ -98,7 +105,7 @@ const graphiqlHapi: IPlugin = {
server.route({
method: 'GET',
path: options.path || '/graphiql',
config: options.route || {},
options: options.route || {},
handler: async (request, h) => {
const graphiqlString = await GraphiQL.resolveGraphiQLString(
request.query,

View file

@ -1,3 +1,14 @@
// Expose types which can be used by both middleware flavors.
export { GraphQLOptions } from 'apollo-server-core';
export {
ApolloError,
toApolloError,
SyntaxError,
ValidationError,
AuthenticationError,
ForbiddenError,
} from 'apollo-server-core';
export {
IRegister,
HapiOptionsFunction,
@ -7,3 +18,6 @@ export {
graphqlHapi,
graphiqlHapi,
} from './hapiApollo';
// ApolloServer integration
export { registerServer } from './ApolloServer';

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -20,16 +20,17 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4",
"apollo-server-module-operation-store": "^1.3.5",
"supertest": "^3.0.0"
},
"devDependencies": {
"@types/graphql": "0.12.7"
"supertest": "^3.1.0"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
},
"devDependencies": {
"@types/node": "^10.1.2",
"graphql-tag": "^2.9.2"
}
}

View file

@ -19,6 +19,7 @@ const request = require('supertest');
import { GraphQLOptions } from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
import { OperationStore } from 'apollo-server-module-operation-store';
import gql from 'graphql-tag';
const personType = new GraphQLObjectType({
name: 'PersonType',
@ -526,6 +527,26 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
});
it('does not accept a query AST', async () => {
app = await createApp();
const expected = {
testString: 'it works',
};
const req = request(app)
.post('/graphql')
.send({
query: gql`
query test {
testString
}
`,
});
return req.then(res => {
expect(res.status).to.equal(400);
expect(res.text).to.contain('GraphQL queries must be strings');
});
});
it('can handle batch requests', async () => {
app = await createApp();
const expected = [
@ -827,7 +848,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
const expected = /at resolveFieldValueOrError/;
const stackTrace = [];
const origError = console.error;
console.error = (...args) => stackTrace.push(args);
const err = stub();
console.error = err;
app = await createApp({
graphqlOptions: {
schema,
@ -841,7 +863,10 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
return req.then(res => {
console.error = origError;
expect(stackTrace[0][0]).to.match(expected);
if (err.called) {
expect(err.calledOnce);
expect(err.getCall(0).args[0]).to.match(expected);
}
});
});
@ -861,8 +886,10 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
return req.then(res => {
logStub.restore();
expect(logStub.callCount).to.equal(1);
expect(logStub.getCall(0).args[0]).to.match(expected);
if (logStub.called) {
expect(logStub.callCount).to.equal(1);
expect(logStub.getCall(0).args[0]).to.match(expected);
}
});
});
@ -992,7 +1019,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
graphqlOptions: {
schema,
formatParams(params) {
params['query'] = store.get(params.operationName);
params['parsedQuery'] = store.get(params.operationName);
delete params['queryString'];
return params;
},
},
@ -1016,10 +1044,10 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
graphqlOptions: {
schema,
formatParams(params) {
if (params.query) {
if (params.queryString) {
throw new Error('Must not provide query, only operationName');
}
params['query'] = store.get(params.operationName);
params['parsedQuery'] = store.get(params.operationName);
return params;
},
},
@ -1051,7 +1079,17 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
]);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal(expected);
expect(res.body.length).to.equal(expected.length);
expect(res.body[0]).to.deep.equal(expected[0]);
if (res.body[1].errors[0].extensions) {
if (res.body[1].errors[0].extensions.code) {
expect(res.body[1].errors[0].extensions.code).to.equal(
'INTERNAL_SERVER_ERROR',
);
}
delete res.body[1].errors[0].extensions;
}
expect(res.body[1]).to.deep.equal(expected[1]);
});
});
});

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -25,16 +25,15 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"devDependencies": {
"@types/graphql": "0.12.7",
"@types/koa": "2.0.45",
"@types/koa-bodyparser": "4.2.0",
"@types/koa-router": "7.0.28",
"apollo-server-integration-testsuite": "^1.3.6",
"koa": "2.5.0",
"koa": "2.5.1",
"koa-bodyparser": "4.2.0",
"koa-router": "7.4.0"
},

View file

@ -3,6 +3,7 @@ import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
convertNodeHttpToRequest,
} from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
@ -33,6 +34,7 @@ export function graphqlKoa(
options: options,
query:
ctx.request.method === 'POST' ? ctx.request.body : ctx.request.query,
request: convertNodeHttpToRequest(ctx.req),
}).then(
gqlResponse => {
ctx.set('Content-Type', 'application/json');

View file

@ -30,7 +30,7 @@ exports.graphiqlHandler = server.graphiqlLambda({
#### 2. Create an S3 bucket
The bucket name name must be universally unique.
The bucket name must be universally unique.
```shell
aws s3 mb s3://<bucket name>

View file

@ -25,12 +25,11 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"devDependencies": {
"@types/aws-lambda": "8.10.2",
"@types/graphql": "0.12.7",
"@types/aws-lambda": "8.10.3",
"apollo-server-integration-testsuite": "^1.3.6"
},
"typings": "dist/index.d.ts",

View file

@ -54,6 +54,7 @@ export function graphqlLambda(
method: event.httpMethod,
options: options,
query: query,
request: event,
});
headers['Content-Type'] = 'application/json';
statusCode = 200;

View file

@ -2,9 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"],
"types": ["@types/node"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -25,11 +25,10 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"devDependencies": {
"@types/graphql": "0.12.7",
"@types/micro": "7.3.1",
"apollo-server-integration-testsuite": "^1.3.6",
"micro": "8.0.4",

View file

@ -2,6 +2,7 @@ import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
convertNodeHttpToRequest,
} from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
import { createError, json, RequestHandler } from 'micro';
@ -42,6 +43,7 @@ export function microGraphql(
method: req.method,
options: options,
query: query,
request: convertNodeHttpToRequest(req),
});
res.setHeader('Content-Type', 'application/json');

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -34,7 +34,7 @@ export type GraphiQLData = {
// Current latest version of GraphiQL.
const GRAPHIQL_VERSION = '0.11.11';
const SUBSCRIPTIONS_TRANSPORT_VERSION = '0.8.2';
const SUBSCRIPTIONS_TRANSPORT_VERSION = '0.9.9';
// Ensures string values are safe to be used within a <script> tag.
// TODO: I don't think that's the right escape function

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -23,9 +23,6 @@
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"devDependencies": {
"@types/graphql": "0.12.7"
},
"peerDependencies": {
"graphql": "^0.9.0 || ^0.10.1 || ^0.11.0 || ^0.12.0 || ^0.13.0"
},

View file

@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
"outDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View file

@ -25,11 +25,10 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6",
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-module-graphiql": "^1.3.4"
},
"devDependencies": {
"@types/graphql": "0.12.7",
"@types/restify": "5.0.7",
"apollo-server-integration-testsuite": "^1.3.6",
"restify": "5.2.1"

View file

@ -4,6 +4,7 @@ import {
GraphQLOptions,
HttpQueryError,
runHttpQuery,
convertNodeHttpToRequest,
} from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
@ -44,6 +45,7 @@ export function graphqlRestify(
method: req.method,
options: options,
query: req.method === 'POST' ? req.body : req.query,
request: convertNodeHttpToRequest(req),
}).then(
gqlResponse => {
res.setHeader('Content-Type', 'application/json');

1
packages/apollo-server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
npm

View file

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

View file

@ -0,0 +1,8 @@
# Changelog
### vNEXT
* `apollo-server`: move non-schema related options into listen [PR#1059](https://github.com/apollographql/apollo-server/pull/1059)
* `apollo-server`: add `bodyParserConfig` options [PR#1059](https://github.com/apollographql/apollo-server/pull/1059)
* `apollo-server`: add `/.well-known/apollo/server-health` endpoint with async callback for additional checks, ie database poke [PR#992](https://github.com/apollographql/apollo-server/pull/992)
* `apollo-server`: collocate graphql gui with endpoint and provide gui when accessed from browser [PR#987](https://github.com/apollographql/apollo-server/pull/987)

View file

@ -0,0 +1,4 @@
# Apollo Server
Apollo Server is a production ready GraphQL Server. [Read the docs.](https://www.apollographql.com/docs/apollo-server/)
[Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md)

View file

@ -0,0 +1,45 @@
{
"name": "apollo-server",
"version": "2.0.0-beta.3",
"description": "Production ready GraphQL Server",
"author": "opensource@apollographql.com",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile",
"watch": "tsc -w"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server"
},
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Javascript"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"devDependencies": {
"@types/body-parser": "^1.17.0",
"@types/express": "^4.11.1",
"@types/request": "^2.47.0",
"request": "^2.87.0",
"typescript": "2.8.1"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"typings": "dist/index.d.ts",
"dependencies": {
"apollo-server-core": "2.0.0-beta.2",
"apollo-server-express": "2.0.0-beta.2",
"express": "^4.16.3",
"graphql-subscriptions": "^0.5.8",
"graphql-tools": "^3.0.1"
}
}

View file

@ -0,0 +1,26 @@
#!/bin/bash -e
# When we publish to npm, the published files are available in the root
# directory, which allows for a clean include or require of sub-modules.
#
# var language = require('apollo-server/express');
#
# Ensure a vanilla package.json before deploying so other tools do not interpret
# The built output as requiring any further transformation.
node -e "var package = require('./package.json'); \
delete package.scripts; \
delete package.private; \
delete package.devDependencies; \
package.main = 'index.js'; \
package.module = 'index.js'; \
package.typings = 'index.d.ts'; \
var origVersion = 'local';
var fs = require('fs'); \
fs.writeFileSync('./npm/package.json', JSON.stringify(package, null, 2)); \
"
# Copy few more files to ./npm
cp README.md npm/
cp ../../LICENSE npm/

View file

@ -0,0 +1,14 @@
export * from 'graphql-tools';
export * from 'graphql-subscriptions';
// this makes it easy to get inline formatting and highlighting without
// actually doing any work
export const gql = String.raw;
export {
ApolloError,
toApolloError,
SyntaxError,
ValidationError,
AuthenticationError,
ForbiddenError,
} from 'apollo-server-core';

View file

@ -0,0 +1,132 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import 'mocha';
import * as request from 'request';
import { createApolloFetch } from 'apollo-fetch';
import { gql, ApolloServer } from './index';
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hi',
},
};
describe('apollo-server', () => {
describe('constructor', () => {
it('accepts typeDefs and resolvers', () => {
expect(() => new ApolloServer({ typeDefs, resolvers })).not.to.throw;
});
it('accepts typeDefs and mocks', () => {
expect(() => new ApolloServer({ typeDefs, mocks: true })).not.to.throw;
});
});
describe('without registerServer', () => {
let server: ApolloServer;
afterEach(async () => {
await server.stop();
});
it('can be queried', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
const { url: uri } = await server.listen();
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;
});
it('renders GraphQL playground when browser requests', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await server.listen();
return new Promise((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();
}
},
);
});
});
it('configures cors', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
const { url: uri } = await server.listen({});
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(
response.response.headers.get('access-control-allow-origin'),
).to.equal('*');
next();
},
);
await apolloFetch({ query: '{hello}' });
});
it('creates a healthcheck endpoint', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
});
const { port } = await server.listen();
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();
}
},
);
});
});
});
});

View file

@ -0,0 +1,39 @@
import * as express from 'express';
import { Application, Request } from 'express';
import { registerServer } from 'apollo-server-express';
import { OptionsJson } from 'body-parser';
import { CorsOptions } from 'cors';
import {
ApolloServerBase,
ListenOptions,
Config,
ServerInfo,
} from 'apollo-server-core';
export * from './exports';
export class ApolloServer extends ApolloServerBase<Request> {
// here we overwrite the underlying listen to configure
// the fallback / default server implementation
async listen(opts: ListenOptions = {}): Promise<ServerInfo> {
// we haven't configured a server yet so lets build the default one
// using express
if (!this.getHttp) {
const app = express();
//provide generous values for the getting started experience
await registerServer({
app,
path: '/',
server: this,
bodyParserConfig: { limit: '50mb' },
cors: {
origin: '*',
},
});
}
return super.listen(opts);
}
}

View file

@ -2,7 +2,9 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
"outDir": "./dist",
"noImplicitAny": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1 +0,0 @@
The `graphql-server-core` package is now called [`apollo-server-core`](https://www.npmjs.com/package/apollo-server-core). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1,33 +0,0 @@
{
"name": "graphql-server-core",
"version": "1.3.6",
"description": "Core engine for Apollo GraphQL server",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-core"
},
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-core": "^1.3.6"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

@ -1 +0,0 @@
export * from 'apollo-server-core';

View file

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

View file

@ -1 +0,0 @@
The `graphql-server-express` package is now called [`apollo-server-express`](https://www.npmjs.com/package/apollo-server-express). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1,35 +0,0 @@
{
"name": "graphql-server-express",
"version": "1.3.6",
"description": "Production-ready Node.js GraphQL server for Express and Connect",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-express"
},
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Express",
"Connect",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-express": "^1.3.6"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

@ -1 +0,0 @@
export * from 'apollo-server-express';

View file

@ -1 +0,0 @@
The `graphql-server-hapi` package is now called [`apollo-server-hapi`](https://www.npmjs.com/package/apollo-server-hapi). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1,34 +0,0 @@
{
"name": "graphql-server-hapi",
"version": "1.3.6",
"description": "Production-ready Node.js GraphQL server for Hapi",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-hapi"
},
"keywords": [
"GraphQL",
"Apollo",
"Hapi",
"Server",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-hapi": "^1.3.6"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

@ -1 +0,0 @@
export * from 'apollo-server-hapi';

View file

@ -1 +0,0 @@
The `graphql-server-koa` package is now called [`apollo-server-koa`](https://www.npmjs.com/package/apollo-server-koa). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1,34 +0,0 @@
{
"name": "graphql-server-koa",
"version": "1.3.6",
"description": "Production-ready Node.js GraphQL server for Koa",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-koa"
},
"keywords": [
"GraphQL",
"Apollo",
"Koa",
"Server",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-koa": "^1.3.6"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

@ -1 +0,0 @@
export * from 'apollo-server-koa';

View file

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

View file

@ -1 +0,0 @@
The `graphql-server-lambda` package is now called [`apollo-server-lambda`](https://www.npmjs.com/package/apollo-server-lambda). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1,34 +0,0 @@
{
"name": "graphql-server-lambda",
"version": "1.3.6",
"description": "Production-ready Node.js GraphQL server for AWS Lambda",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-lambda"
},
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Lambda",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-lambda": "^1.3.6"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

@ -1 +0,0 @@
export * from 'apollo-server-lambda';

View file

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

View file

@ -1 +0,0 @@
The `graphql-server-micro` package is now called [`apollo-server-micro`](https://www.npmjs.com/package/apollo-server-micro). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1,34 +0,0 @@
{
"name": "graphql-server-micro",
"version": "1.3.6",
"description": "Production-ready Node.js GraphQL server for Micro",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/graphql-server-micro"
},
"keywords": [
"GraphQL",
"Apollo",
"Micro",
"Server",
"Javascript"
],
"author": "Nick Nance <nance.nick@gmail.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"dependencies": {
"apollo-server-micro": "^1.3.6"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

@ -1 +0,0 @@
export * from 'apollo-server-micro';

View file

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

View file

@ -1 +0,0 @@
The `graphql-server-module-graphiql` package is now called [`apollo-server-module-graphiql`](https://www.npmjs.com/package/apollo-server-module-graphiql). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

View file

@ -1 +0,0 @@
export * from 'apollo-server-module-graphiql';

View file

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

View file

@ -1 +0,0 @@
The `graphql-server-module-operation-store` package is now called [`apollo-server-module-operation-store`](https://www.npmjs.com/package/apollo-server-module-operation-store). We are continuing to release matching versions of the package under the old name, but we recommend you switch to using the new name. The API is identical.

Some files were not shown because too many files have changed in this diff Show more