mirror of
https://github.com/vale981/apollo-server
synced 2025-03-05 09:41:40 -05:00
bring version-2 up to date
This commit is contained in:
parent
d4a8e1ac7f
commit
170f072f96
7 changed files with 49 additions and 1004 deletions
|
@ -4,10 +4,17 @@ All of the packages in the `apollo-server` repo are released with the same versi
|
||||||
|
|
||||||
### vNEXT
|
### 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)
|
* Remove tests and guaranteed support for Node 4 [PR #1024](https://github.com/apollographql/apollo-server/pull/1024)
|
||||||
* Cleanup docs [PR #1233](https://github.com/apollographql/apollo-server/pull/1233/files)
|
* Cleanup docs [PR #1233](https://github.com/apollographql/apollo-server/pull/1233/files)
|
||||||
|
|
||||||
|
### 1.4.0
|
||||||
|
|
||||||
|
* [Issue #626] Integrate apollo-fastify plugin. [PR #1013](https://github.com/apollographql/apollo-server/pull/1013)
|
||||||
|
* add hapi 16 next() invocation [PR #743](https://github.com/apollographql/apollo-server/pull/743)
|
||||||
|
* Add skipValidation option [PR #839](https://github.com/apollographql/apollo-server/pull/839)
|
||||||
|
* `apollo-server-module-graphiql`: adds an option to the constructor to disable url rewriting when editing a query [PR #1047](https://github.com/apollographql/apollo-server/pull/1047)
|
||||||
|
* Upgrade `subscription-transport-ws` to 0.9.9 for Graphiql
|
||||||
|
|
||||||
### v1.3.6
|
### 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)
|
* 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)
|
||||||
|
|
|
@ -1,10 +1,29 @@
|
||||||
# Apollo Server docs
|
# Documentation
|
||||||
|
|
||||||
Read the docs at [apollographql.com/docs/apollo-server](https://www.apollographql.com/docs/apollo-server/).
|
This is the documentation **source** for this repository.
|
||||||
|
|
||||||
To run the docs app locally:
|
The **deployed** version of the documentation for this repository is available at:
|
||||||
|
|
||||||
```bash
|
* https://www.apollographql.com/docs/apollo-server/
|
||||||
npm install
|
|
||||||
npm start
|
## Documentation for the documentation
|
||||||
```
|
|
||||||
|
This `README.md` is intentionally short since the [documentation for the documentation](https://docs-docs.netlify.com/docs/docs/) provides details for the documentation framework _itself_. Additional information should generally be added to that documentation rather than here in this `README.md`, in order to provide a centralized resource that benefits all documentation deployments.
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
For more information, consult the documentation for the documentation, referenced above.
|
||||||
|
|
||||||
|
In general though:
|
||||||
|
|
||||||
|
* `npm install` in this directory
|
||||||
|
* `npm start` in this directory
|
||||||
|
* Open a browser to the link provided in the console.
|
||||||
|
|
||||||
|
> **Important note:** Changes to the markdown source does not result in an automatic "hot reload" in the browser; it is necessary to reload the page manually in the browser to see it re-rendered. Additionally, changes to `_config.yml` require stopping the server and restarting with `npm start` again.
|
||||||
|
|
||||||
|
## Deploy previews
|
||||||
|
|
||||||
|
Documentation repositories should be setup with a "deploy preview" feature which automatically provides "preview" links in the _status checks_ section of pull-requests.
|
||||||
|
|
||||||
|
In the event that it's not possible to run the documentation locally, pushing changes to the branch for a pull-request can be a suitable alternative that ensures changes to the documentation are properly rendered.
|
||||||
|
|
|
@ -9,12 +9,13 @@
|
||||||
"apollo-hexo-config": "1.0.8",
|
"apollo-hexo-config": "1.0.8",
|
||||||
"chexo": "1.0.5",
|
"chexo": "1.0.5",
|
||||||
"hexo": "3.7.1",
|
"hexo": "3.7.1",
|
||||||
|
"hexo-browsersync": "0.3.0",
|
||||||
"hexo-prism-plus": "1.0.0",
|
"hexo-prism-plus": "1.0.0",
|
||||||
"hexo-renderer-ejs": "0.3.1",
|
"hexo-renderer-ejs": "0.3.1",
|
||||||
"hexo-renderer-less": "0.2.0",
|
"hexo-renderer-less": "0.2.0",
|
||||||
"hexo-renderer-marked": "0.3.2",
|
"hexo-renderer-marked": "0.3.2",
|
||||||
"hexo-server": "0.3.2",
|
"hexo-server": "0.3.2",
|
||||||
"meteor-theme-hexo": "1.0.15"
|
"meteor-theme-hexo": "1.0.16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run build && chexo apollo-hexo-config -- server",
|
"start": "npm run build && chexo apollo-hexo-config -- server",
|
||||||
|
@ -23,6 +24,6 @@
|
||||||
"test": "npm run clean; npm run build"
|
"test": "npm run clean; npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hexo-versioned-netlify-redirects": "^1.0.7"
|
"hexo-versioned-netlify-redirects": "1.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,481 +0,0 @@
|
||||||
// tslint:disable
|
|
||||||
// TODO: enable when you figure out how to automatically fix trailing commas
|
|
||||||
|
|
||||||
// TODO: maybe we should get rid of these tests entirely, and move them to expressApollo.test.ts
|
|
||||||
|
|
||||||
// TODO: wherever possible the tests should be rewritten to make them easily work with Hapi, express, Koa etc.
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Below are the HTTP tests from express-graphql. We're using them here to make
|
|
||||||
* sure apolloServer still works if used in the place of express-graphql.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { graphqlExpress } from './expressApollo';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright (c) 2015, Facebook, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the BSD-style license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree. An additional grant
|
|
||||||
* of patent rights can be found in the PATENTS file in the same directory.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import * as zlib from 'zlib';
|
|
||||||
import * as multer from 'multer';
|
|
||||||
import * as bodyParser from 'body-parser';
|
|
||||||
import * as request from 'supertest';
|
|
||||||
import * as express4 from 'express'; // modern
|
|
||||||
import {
|
|
||||||
GraphQLSchema,
|
|
||||||
GraphQLObjectType,
|
|
||||||
GraphQLNonNull,
|
|
||||||
GraphQLString,
|
|
||||||
GraphQLScalarType,
|
|
||||||
GraphQLError,
|
|
||||||
BREAK,
|
|
||||||
} from 'graphql';
|
|
||||||
|
|
||||||
const QueryRootType = new GraphQLObjectType({
|
|
||||||
name: 'QueryRoot',
|
|
||||||
fields: {
|
|
||||||
test: {
|
|
||||||
type: GraphQLString,
|
|
||||||
args: {
|
|
||||||
who: {
|
|
||||||
type: GraphQLString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolve: (_, args) => 'Hello ' + (args['who'] || 'World'),
|
|
||||||
},
|
|
||||||
thrower: {
|
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
|
||||||
resolve: () => {
|
|
||||||
throw new Error('Throws!');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
custom: {
|
|
||||||
type: GraphQLString,
|
|
||||||
args: {
|
|
||||||
foo: {
|
|
||||||
type: new GraphQLScalarType({
|
|
||||||
name: 'Foo',
|
|
||||||
serialize: v => v,
|
|
||||||
parseValue: () => {
|
|
||||||
throw new Error('Something bad happened');
|
|
||||||
},
|
|
||||||
parseLiteral: () => {
|
|
||||||
throw new Error('Something bad happened');
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: GraphQLString,
|
|
||||||
resolve: (_obj, _args, context) => context,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const TestSchema = new GraphQLSchema({
|
|
||||||
query: QueryRootType,
|
|
||||||
mutation: new GraphQLObjectType({
|
|
||||||
name: 'MutationRoot',
|
|
||||||
fields: {
|
|
||||||
writeTest: {
|
|
||||||
type: QueryRootType,
|
|
||||||
resolve: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function catchError(p) {
|
|
||||||
return p.then(
|
|
||||||
res => {
|
|
||||||
// workaround for unknown issues with testing against npm package of express-graphql.
|
|
||||||
// the same code works when testing against the source, I'm not sure why.
|
|
||||||
if (res && res.error) {
|
|
||||||
return { response: res };
|
|
||||||
}
|
|
||||||
throw new Error('Expected to catch error.');
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
if (!(error instanceof Error)) {
|
|
||||||
throw new Error('Expected error to be instanceof Error.');
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function promiseTo(fn) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fn((error, result) => (error ? reject(error) : resolve(result)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('test harness', () => {
|
|
||||||
it('expects to catch errors', async () => {
|
|
||||||
let caught;
|
|
||||||
try {
|
|
||||||
await catchError(Promise.resolve());
|
|
||||||
} catch (error) {
|
|
||||||
caught = error;
|
|
||||||
}
|
|
||||||
expect(caught && caught.message).to.equal('Expected to catch error.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('expects to catch actual errors', async () => {
|
|
||||||
let caught;
|
|
||||||
try {
|
|
||||||
await catchError(Promise.reject('not a real error'));
|
|
||||||
} catch (error) {
|
|
||||||
caught = error;
|
|
||||||
}
|
|
||||||
expect(caught && caught.message).to.equal(
|
|
||||||
'Expected error to be instanceof Error.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resolves callback promises', async () => {
|
|
||||||
const resolveValue = {};
|
|
||||||
const result = await promiseTo(cb => cb(null, resolveValue));
|
|
||||||
expect(result).to.equal(resolveValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects callback promises with errors', async () => {
|
|
||||||
const rejectError = new Error();
|
|
||||||
let caught;
|
|
||||||
try {
|
|
||||||
await promiseTo(cb => cb(rejectError));
|
|
||||||
} catch (error) {
|
|
||||||
caught = error;
|
|
||||||
}
|
|
||||||
expect(caught).to.equal(rejectError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const express = express4;
|
|
||||||
const version = 'modern';
|
|
||||||
describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
|
|
||||||
describe('POST functionality', () => {
|
|
||||||
it('allows gzipped POST bodies', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress(() => ({
|
|
||||||
schema: TestSchema,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = { query: '{ test(who: "World") }' };
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
// TODO had to write "as any as Buffer" to make tsc accept it. Does it matter?
|
|
||||||
const gzippedJson = await promiseTo(cb =>
|
|
||||||
zlib.gzip((json as any) as Buffer, cb),
|
|
||||||
);
|
|
||||||
|
|
||||||
const req = request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.set('Content-Encoding', 'gzip');
|
|
||||||
req.write(gzippedJson);
|
|
||||||
const response = await req;
|
|
||||||
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
test: 'Hello World',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows deflated POST bodies', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress(() => ({
|
|
||||||
schema: TestSchema,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = { query: '{ test(who: "World") }' };
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
// TODO had to write "as any as Buffer" to make tsc accept it. Does it matter?
|
|
||||||
const deflatedJson = await promiseTo(cb =>
|
|
||||||
zlib.deflate((json as any) as Buffer, cb),
|
|
||||||
);
|
|
||||||
|
|
||||||
const req = request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.set('Content-Encoding', 'deflate');
|
|
||||||
req.write(deflatedJson);
|
|
||||||
const response = await req;
|
|
||||||
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
test: 'Hello World',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows for pre-parsed POST bodies', () => {
|
|
||||||
// Note: this is not the only way to handle file uploads with GraphQL,
|
|
||||||
// but it is terse and illustrative of using express-graphql and multer
|
|
||||||
// together.
|
|
||||||
|
|
||||||
// A simple schema which includes a mutation.
|
|
||||||
const UploadedFileType = new GraphQLObjectType({
|
|
||||||
name: 'UploadedFile',
|
|
||||||
fields: {
|
|
||||||
originalname: { type: GraphQLString },
|
|
||||||
mimetype: { type: GraphQLString },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const TestMutationSchema = new GraphQLSchema({
|
|
||||||
query: new GraphQLObjectType({
|
|
||||||
name: 'QueryRoot',
|
|
||||||
fields: {
|
|
||||||
test: { type: GraphQLString },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
mutation: new GraphQLObjectType({
|
|
||||||
name: 'MutationRoot',
|
|
||||||
fields: {
|
|
||||||
uploadFile: {
|
|
||||||
type: UploadedFileType,
|
|
||||||
resolve(rootValue) {
|
|
||||||
// For this test demo, we're just returning the uploaded
|
|
||||||
// file directly, but presumably you might return a Promise
|
|
||||||
// to go store the file somewhere first.
|
|
||||||
return rootValue.request.file;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Multer provides multipart form data parsing.
|
|
||||||
const storage = multer.memoryStorage();
|
|
||||||
app.use('/graphql', multer({ storage }).single('file'));
|
|
||||||
|
|
||||||
// Providing the request as part of `rootValue` allows it to
|
|
||||||
// be accessible from within Schema resolve functions.
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress(req => {
|
|
||||||
return {
|
|
||||||
schema: TestMutationSchema,
|
|
||||||
rootValue: { request: req },
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const req = request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.field(
|
|
||||||
'query',
|
|
||||||
`mutation TestMutation {
|
|
||||||
uploadFile { originalname, mimetype }
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
.attach('file', __filename);
|
|
||||||
|
|
||||||
return req.then(response => {
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
uploadFile: {
|
|
||||||
originalname: 'apolloServerHttp.test.js',
|
|
||||||
mimetype: 'application/javascript',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Error handling functionality', () => {
|
|
||||||
it('handles field errors caught by GraphQL', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{thrower}',
|
|
||||||
});
|
|
||||||
|
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles type validation', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{notExists}',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
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 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles type validation (GET)', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.get('/graphql')
|
|
||||||
.query({ query: '{notExists}' });
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
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 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles errors thrown during custom graphql type handling', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{custom(foo: 123)}',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles unsupported HTTP methods', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use('/graphql', graphqlExpress({ schema: TestSchema }));
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.put('/graphql')
|
|
||||||
.query({ query: '{test}' });
|
|
||||||
|
|
||||||
expect(response.status).to.equal(405);
|
|
||||||
expect(response.headers.allow).to.equal('GET, POST');
|
|
||||||
expect(response.text).to.contain(
|
|
||||||
'Apollo Server supports only GET/POST requests.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Custom validation rules', () => {
|
|
||||||
const AlwaysInvalidRule = function(context) {
|
|
||||||
return {
|
|
||||||
enter() {
|
|
||||||
context.reportError(
|
|
||||||
new GraphQLError('AlwaysInvalidRule was really invalid!'),
|
|
||||||
);
|
|
||||||
return BREAK;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
it('Do not execute a query if it do not pass the custom validation.', async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use('/graphql', bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/graphql',
|
|
||||||
graphqlExpress({
|
|
||||||
schema: TestSchema,
|
|
||||||
validationRules: [AlwaysInvalidRule],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{thrower}',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
extensions: {
|
|
||||||
code: 'GRAPHQL_VALIDATION_FAILED',
|
|
||||||
},
|
|
||||||
message: 'AlwaysInvalidRule was really invalid!',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from 'apollo-server-core';
|
} from 'apollo-server-core';
|
||||||
|
|
||||||
export interface IRegister {
|
export interface IRegister {
|
||||||
(server: Server, options: any): void;
|
(server: Server, options: any, next?: Function): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlugin {
|
export interface IPlugin {
|
||||||
|
@ -29,7 +29,7 @@ export interface HapiPluginOptions {
|
||||||
|
|
||||||
const graphqlHapi: IPlugin = {
|
const graphqlHapi: IPlugin = {
|
||||||
name: 'graphql',
|
name: 'graphql',
|
||||||
register: (server: Server, options: HapiPluginOptions) => {
|
register: (server: Server, options: HapiPluginOptions, next?: Function) => {
|
||||||
if (!options || !options.graphqlOptions) {
|
if (!options || !options.graphqlOptions) {
|
||||||
throw new Error('Apollo Server requires options.');
|
throw new Error('Apollo Server requires options.');
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,9 @@ const graphqlHapi: IPlugin = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { graphqlHapi };
|
|
||||||
|
|
|
@ -1,507 +0,0 @@
|
||||||
// tslint:disable
|
|
||||||
// TODO: enable when you figure out how to automatically fix trailing commas
|
|
||||||
|
|
||||||
// TODO: maybe we should get rid of these tests entirely, and move them to expressApollo.test.ts
|
|
||||||
|
|
||||||
// TODO: wherever possible the tests should be rewritten to make them easily work with Hapi, express, Koa etc.
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Below are the HTTP tests from koa-graphql. We're using them here to make
|
|
||||||
* sure apolloServer still works if used in the place of koa-graphql.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { graphqlKoa } from './koaApollo';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright (c) 2015, Facebook, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the BSD-style license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree. An additional grant
|
|
||||||
* of patent rights can be found in the PATENTS file in the same directory.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import * as zlib from 'zlib';
|
|
||||||
import * as multer from 'koa-multer';
|
|
||||||
import * as bodyParser from 'koa-bodyparser';
|
|
||||||
import * as KoaRouter from 'koa-router';
|
|
||||||
const request = require('supertest');
|
|
||||||
const Koa = require('koa');
|
|
||||||
import {
|
|
||||||
GraphQLSchema,
|
|
||||||
GraphQLObjectType,
|
|
||||||
GraphQLNonNull,
|
|
||||||
GraphQLString,
|
|
||||||
GraphQLScalarType,
|
|
||||||
GraphQLError,
|
|
||||||
BREAK,
|
|
||||||
} from 'graphql';
|
|
||||||
|
|
||||||
const QueryRootType = new GraphQLObjectType({
|
|
||||||
name: 'QueryRoot',
|
|
||||||
fields: {
|
|
||||||
test: {
|
|
||||||
type: GraphQLString,
|
|
||||||
args: {
|
|
||||||
who: {
|
|
||||||
type: GraphQLString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolve: (_, args) => 'Hello ' + (args['who'] || 'World'),
|
|
||||||
},
|
|
||||||
thrower: {
|
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
|
||||||
resolve: () => {
|
|
||||||
throw new Error('Throws!');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
custom: {
|
|
||||||
type: GraphQLString,
|
|
||||||
args: {
|
|
||||||
foo: {
|
|
||||||
type: new GraphQLScalarType({
|
|
||||||
name: 'Foo',
|
|
||||||
serialize: v => v,
|
|
||||||
parseValue: () => {
|
|
||||||
throw new Error('Something bad happened');
|
|
||||||
},
|
|
||||||
parseLiteral: () => {
|
|
||||||
throw new Error('Something bad happened');
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: GraphQLString,
|
|
||||||
resolve: (_obj, _args, context) => context,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const TestSchema = new GraphQLSchema({
|
|
||||||
query: QueryRootType,
|
|
||||||
mutation: new GraphQLObjectType({
|
|
||||||
name: 'MutationRoot',
|
|
||||||
fields: {
|
|
||||||
writeTest: {
|
|
||||||
type: QueryRootType,
|
|
||||||
resolve: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function catchError(p) {
|
|
||||||
return p.then(
|
|
||||||
res => {
|
|
||||||
// workaround for unknown issues with testing against npm package of koa-graphql.
|
|
||||||
// the same code works when testing against the source, I'm not sure why.
|
|
||||||
if (res && res.error) {
|
|
||||||
return { response: res };
|
|
||||||
}
|
|
||||||
throw new Error('Expected to catch error.');
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
if (!(error instanceof Error)) {
|
|
||||||
throw new Error('Expected error to be instanceof Error.');
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function promiseTo(fn) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fn((error, result) => (error ? reject(error) : resolve(result)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('test harness', () => {
|
|
||||||
it('expects to catch errors', async () => {
|
|
||||||
let caught;
|
|
||||||
try {
|
|
||||||
await catchError(Promise.resolve());
|
|
||||||
} catch (error) {
|
|
||||||
caught = error;
|
|
||||||
}
|
|
||||||
expect(caught && caught.message).to.equal('Expected to catch error.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('expects to catch actual errors', async () => {
|
|
||||||
let caught;
|
|
||||||
try {
|
|
||||||
await catchError(Promise.reject('not a real error'));
|
|
||||||
} catch (error) {
|
|
||||||
caught = error;
|
|
||||||
}
|
|
||||||
expect(caught && caught.message).to.equal(
|
|
||||||
'Expected error to be instanceof Error.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resolves callback promises', async () => {
|
|
||||||
const resolveValue = {};
|
|
||||||
const result = await promiseTo(cb => cb(null, resolveValue));
|
|
||||||
expect(result).to.equal(resolveValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects callback promises with errors', async () => {
|
|
||||||
const rejectError = new Error();
|
|
||||||
let caught;
|
|
||||||
try {
|
|
||||||
await promiseTo(cb => cb(rejectError));
|
|
||||||
} catch (error) {
|
|
||||||
caught = error;
|
|
||||||
}
|
|
||||||
expect(caught).to.equal(rejectError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`GraphQL-HTTP (apolloServer) tests for koa`, () => {
|
|
||||||
describe('POST functionality', () => {
|
|
||||||
it('allows gzipped POST bodies', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa(() => ({
|
|
||||||
schema: TestSchema,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const data = { query: '{ test(who: "World") }' };
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
// TODO had to write "as any as Buffer" to make tsc accept it. Does it matter?
|
|
||||||
const gzippedJson = await promiseTo(cb =>
|
|
||||||
zlib.gzip((json as any) as Buffer, cb),
|
|
||||||
);
|
|
||||||
|
|
||||||
const req = request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.set('Content-Encoding', 'gzip');
|
|
||||||
req.write(gzippedJson);
|
|
||||||
const response = await req;
|
|
||||||
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
test: 'Hello World',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows deflated POST bodies', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa(() => ({
|
|
||||||
schema: TestSchema,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const data = { query: '{ test(who: "World") }' };
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
// TODO had to write "as any as Buffer" to make tsc accept it. Does it matter?
|
|
||||||
const deflatedJson = await promiseTo(cb =>
|
|
||||||
zlib.deflate((json as any) as Buffer, cb),
|
|
||||||
);
|
|
||||||
|
|
||||||
const req = request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.set('Content-Encoding', 'deflate');
|
|
||||||
req.write(deflatedJson);
|
|
||||||
const response = await req;
|
|
||||||
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
test: 'Hello World',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows for pre-parsed POST bodies', () => {
|
|
||||||
// Note: this is not the only way to handle file uploads with GraphQL,
|
|
||||||
// but it is terse and illustrative of using koa-graphql and multer
|
|
||||||
// together.
|
|
||||||
|
|
||||||
// A simple schema which includes a mutation.
|
|
||||||
const UploadedFileType = new GraphQLObjectType({
|
|
||||||
name: 'UploadedFile',
|
|
||||||
fields: {
|
|
||||||
originalname: { type: GraphQLString },
|
|
||||||
mimetype: { type: GraphQLString },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const TestMutationSchema = new GraphQLSchema({
|
|
||||||
query: new GraphQLObjectType({
|
|
||||||
name: 'QueryRoot',
|
|
||||||
fields: {
|
|
||||||
test: { type: GraphQLString },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
mutation: new GraphQLObjectType({
|
|
||||||
name: 'MutationRoot',
|
|
||||||
fields: {
|
|
||||||
uploadFile: {
|
|
||||||
type: UploadedFileType,
|
|
||||||
resolve(rootValue) {
|
|
||||||
// For this test demo, we're just returning the uploaded
|
|
||||||
// file directly, but presumably you might return a Promise
|
|
||||||
// to go store the file somewhere first.
|
|
||||||
return rootValue.request.file;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
// Multer provides multipart form data parsing.
|
|
||||||
const storage = multer.memoryStorage();
|
|
||||||
router.use('/graphql', multer({ storage }).single('file'));
|
|
||||||
|
|
||||||
// Providing the request as part of `rootValue` allows it to
|
|
||||||
// be accessible from within Schema resolve functions.
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa(ctx => {
|
|
||||||
return {
|
|
||||||
schema: TestMutationSchema,
|
|
||||||
rootValue: { request: ctx.req },
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const req = request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.field(
|
|
||||||
'query',
|
|
||||||
`mutation TestMutation {
|
|
||||||
uploadFile { originalname, mimetype }
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
.attach('file', __filename);
|
|
||||||
|
|
||||||
return req.then(response => {
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
uploadFile: {
|
|
||||||
originalname: 'apolloServerHttp.test.js',
|
|
||||||
mimetype: 'application/javascript',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Error handling functionality', () => {
|
|
||||||
it('handles field errors caught by GraphQL', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const response = await request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{thrower}',
|
|
||||||
});
|
|
||||||
|
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles type validation', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const response = await request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{notExists}',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
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 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles type validation (GET)', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const response = await request(app.callback())
|
|
||||||
.get('/graphql')
|
|
||||||
.query({ query: '{notExists}' });
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
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 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles errors thrown during custom graphql type handling', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa({
|
|
||||||
schema: TestSchema,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const response = await request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{custom(foo: 123)}',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles unsupported HTTP methods', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all('/graphql', graphqlKoa({ schema: TestSchema }));
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const response = await request(app.callback())
|
|
||||||
.put('/graphql')
|
|
||||||
.query({ query: '{test}' });
|
|
||||||
|
|
||||||
expect(response.status).to.equal(405);
|
|
||||||
expect(response.headers.allow).to.equal('GET, POST');
|
|
||||||
expect(response.text).to.contain(
|
|
||||||
'Apollo Server supports only GET/POST requests.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Custom validation rules', () => {
|
|
||||||
const AlwaysInvalidRule = function(context) {
|
|
||||||
return {
|
|
||||||
enter() {
|
|
||||||
context.reportError(
|
|
||||||
new GraphQLError('AlwaysInvalidRule was really invalid!'),
|
|
||||||
);
|
|
||||||
return BREAK;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
it('Do not execute a query if it do not pass the custom validation.', async () => {
|
|
||||||
const app = new Koa();
|
|
||||||
const router = new KoaRouter();
|
|
||||||
|
|
||||||
router.use('/graphql', bodyParser());
|
|
||||||
router.all(
|
|
||||||
'/graphql',
|
|
||||||
graphqlKoa({
|
|
||||||
schema: TestSchema,
|
|
||||||
validationRules: [AlwaysInvalidRule],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
const response = await request(app.callback())
|
|
||||||
.post('/graphql')
|
|
||||||
.send({
|
|
||||||
query: '{thrower}',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).to.equal(400);
|
|
||||||
expect(JSON.parse(response.text)).to.deep.equal({
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
extensions: {
|
|
||||||
code: 'GRAPHQL_VALIDATION_FAILED',
|
|
||||||
},
|
|
||||||
message: 'AlwaysInvalidRule was really invalid!',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": [":pinOnlyDevDependencies"],
|
"extends": [":pinOnlyDevDependencies"],
|
||||||
"semanticCommits": true,
|
"semanticCommits": true,
|
||||||
"timezone": "America/Los_Angeles",
|
"timezone": "America/Los_Angeles",
|
||||||
"schedule": ["after 10pm and before 5am on every weekday"],
|
"schedule": ["after 6pm and before 8am on every weekday"],
|
||||||
"rebaseStalePrs": true,
|
"rebaseStalePrs": true,
|
||||||
"prCreation": "not-pending",
|
"prCreation": "not-pending",
|
||||||
"automerge": "minor",
|
"automerge": "minor",
|
||||||
|
@ -10,7 +10,11 @@
|
||||||
"pathRules": [
|
"pathRules": [
|
||||||
{
|
{
|
||||||
"paths": ["docs/package.json"],
|
"paths": ["docs/package.json"],
|
||||||
"extends": ["apollo-docs"]
|
"extends": ["apollo-docs"],
|
||||||
|
"baseBranches": [
|
||||||
|
"master",
|
||||||
|
"version-2"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
|
Loading…
Add table
Reference in a new issue