mirror of
https://github.com/vale981/apollo-server
synced 2025-03-05 09:41:40 -05:00
Merge pull request #1063 from apollographql/refactor-2.0
Apollo Server 2.0
This commit is contained in:
commit
639b104232
109 changed files with 3773 additions and 718 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@ subtitle: Apollo Server
|
|||
description: A guide to using Apollo Server.
|
||||
versions:
|
||||
- '1'
|
||||
- '2'
|
||||
sidebar_categories:
|
||||
null:
|
||||
- index
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"tag: internal": ":house: Internal"
|
||||
}
|
||||
},
|
||||
"packages": [
|
||||
"packages/*"
|
||||
]
|
||||
"hoist": true,
|
||||
"packages": ["packages/*"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"types": ["@types/node"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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",
|
18
packages/apollo-server-cloudflare/src/ApolloServer.ts
Normal file
18
packages/apollo-server-cloudflare/src/ApolloServer.ts
Normal 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 };
|
||||
}
|
||||
}
|
68
packages/apollo-server-cloudflare/src/cloudflareApollo.ts
Normal file
68
packages/apollo-server-cloudflare/src/cloudflareApollo.ts
Normal 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;
|
||||
}
|
2
packages/apollo-server-cloudflare/src/index.ts
Normal file
2
packages/apollo-server-cloudflare/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { graphqlCloudflare } from './cloudflareApollo';
|
||||
export { ApolloServer } from './ApolloServer';
|
|
@ -4,5 +4,6 @@
|
|||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
12
packages/apollo-server-core/CHANGELOG.md
Normal file
12
packages/apollo-server-core/CHANGELOG.md
Normal 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)
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
825
packages/apollo-server-core/src/ApolloServer.test.ts
Normal file
825
packages/apollo-server-core/src/ApolloServer.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
381
packages/apollo-server-core/src/ApolloServer.ts
Normal file
381
packages/apollo-server-core/src/ApolloServer.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
174
packages/apollo-server-core/src/errors.test.ts
Normal file
174
packages/apollo-server-core/src/errors.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
266
packages/apollo-server-core/src/errors.ts
Normal file
266
packages/apollo-server-core/src/errors.ts
Normal 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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
25
packages/apollo-server-core/src/logging.ts
Normal file
25
packages/apollo-server-core/src/logging.ts
Normal 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);
|
||||
}
|
19
packages/apollo-server-core/src/nodeHttpToRequest.ts
Normal file
19
packages/apollo-server-core/src/nodeHttpToRequest.ts
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
90
packages/apollo-server-core/src/types.ts
Normal file
90
packages/apollo-server-core/src/types.ts
Normal 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;
|
||||
}
|
|
@ -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": {
|
||||
|
|
499
packages/apollo-server-express/src/ApolloServer.test.ts
Normal file
499
packages/apollo-server-express/src/ApolloServer.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
143
packages/apollo-server-express/src/ApolloServer.ts
Normal file
143
packages/apollo-server-express/src/ApolloServer.ts
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
|
@ -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!',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
199
packages/apollo-server-hapi/src/ApolloServer.ts
Normal file
199
packages/apollo-server-hapi/src/ApolloServer.ts
Normal 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 });
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -54,6 +54,7 @@ export function graphqlLambda(
|
|||
method: event.httpMethod,
|
||||
options: options,
|
||||
query: query,
|
||||
request: event,
|
||||
});
|
||||
headers['Content-Type'] = 'application/json';
|
||||
statusCode = 200;
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"types": ["@types/node"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
1
packages/apollo-server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
npm
|
6
packages/apollo-server/.npmignore
Normal file
6
packages/apollo-server/.npmignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
*
|
||||
!src/**/*
|
||||
!dist/**/*
|
||||
dist/**/*.test.*
|
||||
!package.json
|
||||
!README.md
|
8
packages/apollo-server/CHANGELOG.md
Normal file
8
packages/apollo-server/CHANGELOG.md
Normal 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)
|
4
packages/apollo-server/README.md
Normal file
4
packages/apollo-server/README.md
Normal 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)
|
45
packages/apollo-server/package.json
Normal file
45
packages/apollo-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
26
packages/apollo-server/scripts/prepare-package.sh
Executable file
26
packages/apollo-server/scripts/prepare-package.sh
Executable 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/
|
14
packages/apollo-server/src/exports.ts
Normal file
14
packages/apollo-server/src/exports.ts
Normal 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';
|
132
packages/apollo-server/src/index.test.ts
Normal file
132
packages/apollo-server/src/index.test.ts
Normal 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();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
39
packages/apollo-server/src/index.ts
Normal file
39
packages/apollo-server/src/index.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,9 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
"outDir": "./dist",
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-core';
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-express';
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-hapi';
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-koa';
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-lambda';
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-micro';
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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.
|
|
@ -1 +0,0 @@
|
|||
export * from 'apollo-server-module-graphiql';
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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
Loading…
Add table
Reference in a new issue