feat(fastify) Apollo Fastify server integration resolve #626

This commit is contained in:
Remy Korrelboom 2018-11-15 08:35:52 +01:00
parent ccd974dbc6
commit bacdeae80d
17 changed files with 1411 additions and 0 deletions

View file

@ -39,6 +39,7 @@ client reference ID, Apollo Server will now default to the values present in the
of the request (`apollographql-client-name`, `apollographql-client-reference-id` and
`apollographql-client-version` respectively). As a last resort, when those headers are not set,
the query extensions' `clientInfo` values will be used. [PR #1960](https://github.com/apollographql/apollo-server/pull/1960)
- Added `apollo-server-fastify` integration ([@rkorrelboom](https://github.com/rkorrelboom) in [#1971](https://github.com/apollostack/apollo-server/pull/1971))
### v2.2.2

View file

@ -235,6 +235,29 @@ new ApolloServer({
})
```
## Fastify
```js
const { ApolloServer, gql } = require('apollo-server-fastify');
const fastify = require('fastify');
async function StartServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const app = fastify();
await server.applyMiddleware({
app,
});
await server.installSubscriptionHandlers(app.server);
await app.listen(3000);
}
StartServer().catch(error => console.log(error));
```
### AWS Lambda
Apollo Server can be run on Lambda and deployed with AWS Serverless Application Model (SAM). It requires an API Gateway with Lambda Proxy Integration.

View file

@ -51,6 +51,7 @@
"apollo-server-env": "file:packages/apollo-server-env",
"apollo-server-errors": "file:packages/apollo-server-errors",
"apollo-server-express": "file:packages/apollo-server-express",
"apollo-server-fastify": "file:packages/apollo-server-fastify",
"apollo-server-hapi": "file:packages/apollo-server-hapi",
"apollo-server-integration-testsuite": "file:packages/apollo-server-integration-testsuite",
"apollo-server-koa": "file:packages/apollo-server-koa",
@ -94,6 +95,7 @@
"codecov": "3.1.0",
"connect": "3.6.6",
"express": "4.16.4",
"fastify": "1.13.0",
"fibers": "3.1.1",
"form-data": "2.3.3",
"graphql": "14.0.2",

View file

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

View file

@ -0,0 +1,45 @@
---
title: Fastify
description: Setting up Apollo Server with Fastify
---
[![npm version](https://badge.fury.io/js/apollo-server-fastify.svg)](https://badge.fury.io/js/apollo-server-fastify) [![Build Status](https://circleci.com/gh/apollographql/apollo-server.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-server) [![Coverage Status](https://coveralls.io/repos/github/apollographql/apollo-server/badge.svg?branch=master)](https://coveralls.io/github/apollographql/apollo-server?branch=master) [![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://www.apollographql.com/#slack)
This is the Fastify integration of GraphQL Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/). [Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md)
```sh
npm install apollo-server-fastify
```
## Fastify
```js
const { ApolloServer, gql } = require('apollo-server-fastify');
const fastify = require('fastify');
async function StartServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const app = fastify();
await server.applyMiddleware({
app,
});
await server.installSubscriptionHandlers(app.server);
await app.listen(3000);
}
StartServer().catch(error => console.log(error));
```
## Principles
GraphQL Server is built with the following principles in mind:
* **By the community, for the community**: GraphQL Server's development is driven by the needs of developers
* **Simplicity**: by keeping things simple, GraphQL Server is easier to use, easier to contribute to, and more secure
* **Performance**: GraphQL Server is well-tested and production-ready - no modifications needed
Anyone is welcome to contribute to GraphQL Server, just read [CONTRIBUTING.md](https://github.com/apollographql/apollo-server/blob/master/CONTRIBUTING.md), take a look at the [roadmap](https://github.com/apollographql/apollo-server/blob/master/ROADMAP.md) and make your first PR!

View file

@ -0,0 +1,3 @@
const config = require('../../jest.config.base');
module.exports = Object.assign(Object.create(null), config);

View file

@ -0,0 +1,42 @@
{
"name": "apollo-server-fastify",
"version": "2.2.2",
"description": "Production-ready Node.js GraphQL server for Fastify",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-fastify"
},
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Fastify",
"Javascript"
],
"author": "opensource@apollographql.com",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"engines": {
"node": ">=6"
},
"dependencies": {
"@apollographql/apollo-upload-server": "^5.0.3",
"@apollographql/graphql-playground-html": "^1.6.4",
"apollo-server-core": "file:../apollo-server-core",
"fastify-accepts": "^0.5.0",
"fastify-cors": "^0.2.0",
"graphql-subscriptions": "^1.0.0",
"graphql-tools": "^4.0.0"
},
"devDependencies": {
"apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
}
}

View file

@ -0,0 +1,145 @@
import { renderPlaygroundPage } from '@apollographql/graphql-playground-html';
import { Accepts } from 'accepts';
import {
ApolloServerBase,
PlaygroundRenderPageOptions,
} from 'apollo-server-core';
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { IncomingMessage, OutgoingMessage } from 'http';
import { processRequest as processFileUploads } from '@apollographql/apollo-upload-server';
import { graphqlFastify } from './fastifyApollo';
const fastJson = require('fast-json-stringify');
export interface ServerRegistration {
app: FastifyInstance;
path?: string;
cors?: object | boolean;
onHealthCheck?: (req: FastifyRequest<IncomingMessage>) => Promise<any>;
disableHealthCheck?: boolean;
}
const stringifyHealthCheck = fastJson({
type: 'object',
properties: {
status: {
type: 'string',
},
},
});
export class ApolloServer extends ApolloServerBase {
protected supportsSubscriptions(): boolean {
return true;
}
protected supportsUploads(): boolean {
return true;
}
public async applyMiddleware({
app,
path,
cors,
disableHealthCheck,
onHealthCheck,
}: ServerRegistration) {
await this.willStart();
if (!path) path = '/graphql';
this.graphqlPath = path;
app.register(require('fastify-accepts'));
if (!disableHealthCheck) {
app.get('/.well-known/apollo/server-health', async (req, res) => {
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');
if (onHealthCheck) {
try {
await onHealthCheck(req);
res.send(stringifyHealthCheck({ status: 'pass' }));
} catch (e) {
res.status(503).send(stringifyHealthCheck({ status: 'fail' }));
}
} else {
res.send(stringifyHealthCheck({ status: 'pass' }));
}
});
}
if (cors === true) {
app.register(require('fastify-cors'));
} else if (cors !== false) {
app.register(require('fastify-cors'), cors);
}
app.register(
async instance => {
instance.setNotFoundHandler((_request, reply) => {
reply.code(405);
reply.header('allow', 'GET, POST');
reply.send();
});
instance.addContentTypeParser(
'multipart',
async (request: IncomingMessage) =>
processFileUploads(request, this.uploadsConfig),
);
instance.register(graphqlFastify, {
route: {
beforeHandler: (
req: FastifyRequest<IncomingMessage>,
reply: FastifyReply<OutgoingMessage>,
done: () => void,
) => {
// Note: if you enable playground in production and expect to be able to see your
// schema, you'll need to manually specify `introspection: true` in the
// ApolloServer constructor; by default, the introspection query is only
// enabled in dev.
if (this.playgroundOptions && req.req.method === 'GET') {
// perform more expensive content-type check only if necessary
const accept = (req as any).accepts() as Accepts;
const types = accept.types() as string[];
const prefersHTML =
types.find(
(x: string) =>
x === 'text/html' || x === 'application/json',
) === 'text/html';
if (prefersHTML) {
const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
endpoint: path,
subscriptionEndpoint: this.subscriptionsPath,
...this.playgroundOptions,
};
reply.type('text/html');
const playground = renderPlaygroundPage(
playgroundRenderPageOptions,
);
reply.send(playground);
return;
}
}
done();
},
},
graphqlOptions: this.graphQLServerOptions.bind(this),
});
},
{
prefix: path,
},
);
}
}
export const registerServer = () => {
throw new Error(
'Please use server.applyMiddleware instead of registerServer. This warning will be removed in the next release',
);
};

View file

@ -0,0 +1,835 @@
import { FastifyInstance } from 'fastify';
import fastify from 'fastify';
import http from 'http';
import request from 'request';
import FormData from 'form-data';
import fs from 'fs';
import { createApolloFetch } from 'apollo-fetch';
import { gql, AuthenticationError, Config } from 'apollo-server-core';
import { ApolloServer, ServerRegistration } from '../ApolloServer';
import {
atLeastMajorNodeVersion,
testApolloServer,
createServerInfo,
} from 'apollo-server-integration-testsuite';
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'hi',
},
};
const port = 8888;
describe('apollo-server-fastify', () => {
let server: ApolloServer;
let httpServer: http.Server;
let app: FastifyInstance;
testApolloServer(
async options => {
server = new ApolloServer(options);
app = fastify();
await server.applyMiddleware({ app });
await app.listen(port);
return createServerInfo(server, app.server);
},
async () => {
if (server) await server.stop();
if (app) await new Promise(resolve => app.close(() => resolve()));
if (httpServer && httpServer.listening) await httpServer.close();
},
);
});
describe('apollo-server-fastify', () => {
let server: ApolloServer;
let app: FastifyInstance;
let httpServer: http.Server;
async function createServer(
serverOptions: Config,
options: Partial<ServerRegistration> = {},
) {
server = new ApolloServer(serverOptions);
app = fastify();
await server.applyMiddleware({ ...options, app });
await app.listen(port);
return createServerInfo(server, app.server);
}
afterEach(async () => {
if (server) await server.stop();
if (app) await new Promise(resolve => app.close(() => resolve()));
if (httpServer) await httpServer.close();
});
describe('constructor', async () => {
it('accepts typeDefs and resolvers', () => {
return createServer({ typeDefs, resolvers });
});
});
describe('applyMiddleware', async () => {
it('can be queried', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.data).toEqual({ hello: 'hi' });
expect(result.errors).toBeUndefined();
});
// XXX Unclear why this would be something somebody would want (vs enabling
// introspection without graphql-playground, which seems reasonable, eg you
// have your own graphql-playground setup with a custom link)
it('can enable playground separately from introspection during production', async () => {
const INTROSPECTION_QUERY = `
{
__schema {
directives {
name
}
}
}
`;
const { url: uri } = await createServer({
typeDefs,
resolvers,
introspection: false,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: INTROSPECTION_QUERY });
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual(
'GRAPHQL_VALIDATION_FAILED',
);
return new Promise<http.Server>((resolve, reject) => {
request(
{
url: uri,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
},
},
(error, response, body) => {
if (error) {
reject(error);
} else {
expect(body).toMatch('GraphQLPlayground');
expect(response.statusCode).toEqual(200);
resolve();
}
},
);
});
});
it('renders GraphQL playground by default when browser requests', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const { url } = await createServer({
typeDefs,
resolvers,
});
return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
},
},
(error, response, body) => {
process.env.NODE_ENV = nodeEnv;
if (error) {
reject(error);
} else {
expect(body).toMatch('GraphQLPlayground');
expect(body).not.toMatch('settings');
expect(response.statusCode).toEqual(200);
resolve();
}
},
);
});
});
const playgroundPartialOptionsTest = async () => {
const defaultQuery = 'query { foo { bar } }';
const endpoint = '/fumanchupacabra';
const { url } = await createServer(
{
typeDefs,
resolvers,
playground: {
// https://github.com/apollographql/graphql-playground/blob/0e452d2005fcd26f10fbdcc4eed3b2e2af935e3a/packages/graphql-playground-html/src/render-playground-page.ts#L16-L24
// must be made partial
settings: {
'editor.theme': 'light',
} as any,
tabs: [
{
query: defaultQuery,
},
{
endpoint,
} as any,
],
},
},
{},
);
return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
Folo: 'bar',
},
},
(error, response, body) => {
if (error) {
reject(error);
} else {
expect(body).toMatch('GraphQLPlayground');
expect(body).toMatch(`"editor.theme": "light"`);
expect(body).toMatch(defaultQuery);
expect(body).toMatch(endpoint);
expect(response.statusCode).toEqual(200);
resolve();
}
},
);
});
};
it('accepts partial GraphQL Playground Options in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
await playgroundPartialOptionsTest();
process.env.NODE_ENV = nodeEnv;
});
it(
'accepts partial GraphQL Playground Options when an environment is ' +
'not specified',
async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
await playgroundPartialOptionsTest();
process.env.NODE_ENV = nodeEnv;
},
);
it('accepts playground options as a boolean', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const { url } = await createServer(
{
typeDefs,
resolvers,
playground: false,
},
{},
);
return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
},
},
(error, response, body) => {
process.env.NODE_ENV = nodeEnv;
if (error) {
reject(error);
} else {
expect(body).not.toMatch('GraphQLPlayground');
expect(response.statusCode).not.toEqual(200);
resolve();
}
},
);
});
});
it('accepts cors configuration', async () => {
const { url: uri } = await createServer(
{
typeDefs,
resolvers,
},
{
cors: { origin: 'apollographql.com' },
},
);
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(
response.response.headers.get('access-control-allow-origin'),
).toEqual('apollographql.com');
next();
},
);
await apolloFetch({ query: '{hello}' });
});
describe('healthchecks', () => {
afterEach(async () => {
await server.stop();
});
it('creates a healthcheck endpoint', async () => {
const { port } = await createServer({
typeDefs,
resolvers,
});
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).toEqual(JSON.stringify({ status: 'pass' }));
expect(response.statusCode).toEqual(200);
resolve();
}
},
);
});
});
it('provides a callback for the healthcheck', async () => {
const { port } = await createServer(
{
typeDefs,
resolvers,
},
{
onHealthCheck: async () => {
throw Error("can't connect to DB");
},
},
);
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).toEqual(JSON.stringify({ status: 'fail' }));
expect(response.statusCode).toEqual(503);
resolve();
}
},
);
});
});
it('can disable the healthCheck', async () => {
const { port } = await createServer(
{
typeDefs,
resolvers,
},
{
disableHealthCheck: true,
},
);
return new Promise((resolve, reject) => {
request(
{
url: `http://localhost:${port}/.well-known/apollo/server-health`,
method: 'GET',
},
(error, response) => {
if (error) {
reject(error);
} else {
expect(response.statusCode).toEqual(404);
resolve();
}
},
);
});
});
});
// NODE: Intentionally skip file upload tests on Node.js 10 or higher.
(atLeastMajorNodeVersion(10) ? describe.skip : describe)(
'file uploads',
() => {
it('enabled uploads', async () => {
const { port } = await createServer({
typeDefs: gql`
type File {
filename: String!
mimetype: String!
encoding: String!
}
type Query {
uploads: [File]
}
type Mutation {
singleUpload(file: Upload!): File!
}
`,
resolvers: {
Query: {
uploads: () => {},
},
Mutation: {
singleUpload: async (_, args) => {
expect((await args.file).stream).toBeDefined();
return args.file;
},
},
},
});
const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: `
mutation($file: Upload!) {
singleUpload(file: $file) {
filename
encoding
mimetype
}
}
`,
variables: {
file: null,
},
}),
);
body.append('map', JSON.stringify({ 1: ['variables.file'] }));
body.append('1', fs.createReadStream('package.json'));
try {
const resolved = await fetch(`http://localhost:${port}/graphql`, {
method: 'POST',
body: body as any,
});
const text = await resolved.text();
const response = JSON.parse(text);
expect(response.data.singleUpload).toEqual({
filename: 'package.json',
encoding: '7bit',
mimetype: 'application/json',
});
} catch (error) {
// This error began appearing randomly and seems to be a dev dependency bug.
// https://github.com/jaydenseric/apollo-upload-server/blob/18ecdbc7a1f8b69ad51b4affbd986400033303d4/test.js#L39-L42
if (error.code !== 'EPIPE') throw error;
}
});
},
);
describe('errors', () => {
it('returns thrown context error as a valid graphql result', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => {
throw Error('never get here');
},
},
};
const { url: uri } = await createServer({
typeDefs,
resolvers,
context: () => {
throw new AuthenticationError('valid result');
},
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: '{hello}' });
expect(result.errors.length).toEqual(1);
expect(result.data).toBeUndefined();
const e = result.errors[0];
expect(e.message).toMatch('valid result');
expect(e.extensions).toBeDefined();
expect(e.extensions.code).toEqual('UNAUTHENTICATED');
expect(e.extensions.exception.stacktrace).toBeDefined();
process.env.NODE_ENV = nodeEnv;
});
it('propogates error codes in dev mode', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const { url: uri } = await createServer({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
expect(result.data).toBeDefined();
expect(result.data).toEqual({ error: null });
expect(result.errors).toBeDefined();
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).toBeDefined();
expect(result.errors[0].extensions.exception.stacktrace).toBeDefined();
process.env.NODE_ENV = nodeEnv;
});
it('propogates error codes in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const { url: uri } = await createServer({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
expect(result.data).toBeDefined();
expect(result.data).toEqual({ error: null });
expect(result.errors).toBeDefined();
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).toBeUndefined();
process.env.NODE_ENV = nodeEnv;
});
it('propogates error codes with null response in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const { url: uri } = await createServer({
typeDefs: gql`
type Query {
error: String!
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({ query: `{error}` });
expect(result.data).toBeNull();
expect(result.errors).toBeDefined();
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).toBeUndefined();
process.env.NODE_ENV = nodeEnv;
});
});
});
describe('extensions', () => {
const books = [
{
title: 'H',
author: 'J',
},
];
const typeDefs = gql`
type Book {
title: String
author: String
}
type Cook @cacheControl(maxAge: 200) {
title: String
author: String
}
type Pook @cacheControl(maxAge: 200) {
title: String
books: [Book] @cacheControl(maxAge: 20, scope: PRIVATE)
}
type Query {
books: [Book]
cooks: [Cook]
pooks: [Pook]
}
`;
const resolvers = {
Query: {
books: () => books,
cooks: () => books,
pooks: () => [{ title: 'pook', books }],
},
};
describe('Cache Control Headers', () => {
it('applies cacheControl Headers and strips out extension', async () => {
const { url: uri } = await createServer({ typeDefs, resolvers });
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).toEqual(
'max-age=200, public',
);
next();
},
);
const result = await apolloFetch({
query: `{ cooks { title author } }`,
});
expect(result.data).toEqual({ cooks: books });
expect(result.extensions).toBeUndefined();
});
it('contains no cacheControl Headers and keeps extension with engine proxy', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
cacheControl: true,
});
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).toBeNull();
next();
},
);
const result = await apolloFetch({
query: `{ cooks { title author } }`,
});
expect(result.data).toEqual({ cooks: books });
expect(result.extensions).toBeDefined();
expect(result.extensions.cacheControl).toBeDefined();
});
it('contains no cacheControl Headers when uncachable', async () => {
const { url: uri } = await createServer({ typeDefs, resolvers });
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).toBeNull();
next();
},
);
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).toEqual({ books });
expect(result.extensions).toBeUndefined();
});
it('contains private cacheControl Headers when scoped', async () => {
const { url: uri } = await createServer({ typeDefs, resolvers });
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).toEqual(
'max-age=20, private',
);
next();
},
);
const result = await apolloFetch({
query: `{ pooks { title books { title author } } }`,
});
expect(result.data).toEqual({
pooks: [{ title: 'pook', books }],
});
expect(result.extensions).toBeUndefined();
});
it('runs when cache-control is false', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
cacheControl: false,
});
const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
expect(response.response.headers.get('cache-control')).toBeNull();
next();
},
);
const result = await apolloFetch({
query: `{ pooks { title books { title author } } }`,
});
expect(result.data).toEqual({
pooks: [{ title: 'pook', books }],
});
expect(result.extensions).toBeUndefined();
});
});
describe('Tracing', () => {
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`;
const resolvers = {
Query: {
books: () => books,
},
};
it('applies tracing extension', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
tracing: true,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).toEqual({ books });
expect(result.extensions).toBeDefined();
expect(result.extensions.tracing).toBeDefined();
});
it('applies tracing extension with cache control enabled', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
tracing: true,
cacheControl: true,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).toEqual({ books });
expect(result.extensions).toBeDefined();
expect(result.extensions.tracing).toBeDefined();
});
xit('applies tracing extension with engine enabled', async () => {
const { url: uri } = await createServer({
typeDefs,
resolvers,
tracing: true,
engine: {
apiKey: 'service:my-app:secret',
maxAttempts: 0,
endpointUrl: 'l',
reportErrorFunction: () => {},
},
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
expect(result.data).toEqual({ books });
expect(result.extensions).toBeDefined();
expect(result.extensions.tracing).toBeDefined();
});
});
});
});

View file

@ -0,0 +1,143 @@
import fastify, { FastifyInstance } from 'fastify';
import { RESTDataSource } from 'apollo-datasource-rest';
import { createApolloFetch } from 'apollo-fetch';
import { ApolloServer } from '../ApolloServer';
import { createServerInfo } from 'apollo-server-integration-testsuite';
import { gql } from '../index';
const restPort = 4001;
export class IdAPI extends RESTDataSource {
baseURL = `http://localhost:${restPort}/`;
async getId(id: string) {
return this.get(`id/${id}`);
}
async getStringId(id: string) {
return this.get(`str/${id}`);
}
}
const typeDefs = gql`
type Query {
id: String
stringId: String
}
`;
const resolvers = {
Query: {
id: async (_source, _args, { dataSources }) => {
return (await dataSources.id.getId('hi')).id;
},
stringId: async (_source, _args, { dataSources }) => {
return dataSources.id.getStringId('hi');
},
},
};
let restCalls = 0;
const restAPI = fastify();
restAPI.get('/id/:id', (req, res) => {
const id = req.params.id;
restCalls++;
res.header('Content-Type', 'application/json');
res.header('Cache-Control', 'max-age=2000, public');
// res.write(JSON.stringify());
res.send({ id });
});
restAPI.get('/str/:id', (req, res) => {
const id = req.params.id;
restCalls++;
res.header('Content-Type', 'text/plain');
res.header('Cache-Control', 'max-age=2000, public');
// res.write(id);
res.send(id);
});
describe('apollo-server-fastify', () => {
let restServer: FastifyInstance;
let app: FastifyInstance;
beforeAll(async () => {
await restAPI.listen(restPort);
});
afterAll(async () => {
await new Promise(resolve => restServer.close(() => resolve()));
});
let server: ApolloServer;
beforeEach(() => {
restCalls = 0;
});
afterEach(async () => {
await server.stop();
// await httpServer.close();
await new Promise(resolve => app.close(() => resolve()));
});
it('uses the cache', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
id: new IdAPI(),
}),
});
app = fastify();
await server.applyMiddleware({ app });
await app.listen(6667);
const { url: uri } = createServerInfo(server, app.server);
const apolloFetch = createApolloFetch({ uri });
const firstResult = await apolloFetch({ query: '{ id }' });
expect(firstResult.data).toEqual({ id: 'hi' });
expect(firstResult.errors).toBeUndefined();
expect(restCalls).toEqual(1);
const secondResult = await apolloFetch({ query: '{ id }' });
expect(secondResult.data).toEqual({ id: 'hi' });
expect(secondResult.errors).toBeUndefined();
expect(restCalls).toEqual(1);
});
it('can cache a string from the backend', async () => {
server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
id: new IdAPI(),
}),
});
app = fastify();
server.applyMiddleware({ app });
await app.listen(6668);
const { url: uri } = createServerInfo(server, app.server);
const apolloFetch = createApolloFetch({ uri });
const firstResult = await apolloFetch({ query: '{ id: stringId }' });
expect(firstResult.data).toEqual({ id: 'hi' });
expect(firstResult.errors).toBeUndefined();
expect(restCalls).toEqual(1);
const secondResult = await apolloFetch({ query: '{ id: stringId }' });
expect(secondResult.data).toEqual({ id: 'hi' });
expect(secondResult.errors).toBeUndefined();
expect(restCalls).toEqual(1);
});
});

View file

@ -0,0 +1,38 @@
import fastify from 'fastify';
import { Server } from 'http';
import { ApolloServer } from '../ApolloServer';
import testSuite, {
schema as Schema,
CreateAppOptions,
} from 'apollo-server-integration-testsuite';
import { GraphQLOptions, Config } from 'apollo-server-core';
async function createApp(options: CreateAppOptions = {}) {
const app = fastify();
const server = new ApolloServer(
(options.graphqlOptions as Config) || { schema: Schema },
);
await server.applyMiddleware({ app });
await app.listen();
return app.server;
}
async function destroyApp(app: Server) {
if (!app || !app.close) {
return;
}
await new Promise(resolve => app.close(resolve));
}
describe('fastifyApollo', () => {
it('throws error if called without schema', function() {
expect(() => new ApolloServer(undefined as GraphQLOptions)).toThrow(
'ApolloServer requires options.',
);
});
});
describe('integration:Fastify', () => {
testSuite(createApp, destroyApp);
});

View file

@ -0,0 +1,8 @@
{
"extends": "../../../../tsconfig.test.base",
"include": ["**/*"],
"references": [
{ "path": "../../" },
{ "path": "../../../apollo-server-integration-testsuite" }
]
}

View file

@ -0,0 +1,77 @@
import {
convertNodeHttpToRequest,
GraphQLOptions,
runHttpQuery,
} from 'apollo-server-core';
import {
FastifyInstance,
FastifyReply,
FastifyRequest,
RegisterOptions,
RouteOptions,
} from 'fastify';
import { IncomingMessage, OutgoingMessage, Server } from 'http';
export interface FastifyGraphQLOptionsFunction
extends RegisterOptions<Server, IncomingMessage, OutgoingMessage> {
route: Partial<RouteOptions<Server, IncomingMessage, OutgoingMessage>>;
graphqlOptions: (
req?: FastifyRequest<IncomingMessage>,
res?: FastifyReply<OutgoingMessage>,
) => GraphQLOptions | Promise<GraphQLOptions>;
}
export async function graphqlFastify(
fastify: FastifyInstance<Server, IncomingMessage, OutgoingMessage>,
options: FastifyGraphQLOptionsFunction,
): Promise<void> {
if (!options) {
throw new Error('Apollo Server requires options.');
}
fastify.route({
method: ['GET', 'POST'],
url: '/',
handler: async (
request: FastifyRequest<IncomingMessage>,
reply: FastifyReply<OutgoingMessage>,
) => {
try {
const { graphqlResponse, responseInit } = await runHttpQuery(
[request, reply],
{
method: request.req.method as string,
options: options.graphqlOptions,
query: request.req.method === 'POST' ? request.body : request.query,
request: convertNodeHttpToRequest(request.raw),
},
);
if (responseInit.headers) {
for (const [name, value] of Object.entries<string>(
responseInit.headers,
)) {
reply.header(name, value);
}
}
reply.serializer((payload: string) => payload);
reply.send(graphqlResponse);
} catch (error) {
if ('HttpQueryError' !== error.name) {
throw error;
}
if (error.headers) {
Object.keys(error.headers).forEach(header => {
reply.header(header, error.headers[header]);
});
}
reply.code(error.statusCode);
reply.serializer((payload: string) => payload);
reply.send(error.message);
}
},
...options.route,
});
}

View file

@ -0,0 +1,29 @@
export {
GraphQLUpload,
GraphQLOptions,
GraphQLExtension,
Config,
gql,
// Errors
ApolloError,
toApolloError,
SyntaxError,
ValidationError,
AuthenticationError,
ForbiddenError,
UserInputError,
// playground
defaultPlaygroundOptions,
PlaygroundConfig,
PlaygroundRenderPageOptions,
} from 'apollo-server-core';
export * from 'graphql-tools';
export * from 'graphql-subscriptions';
// ApolloServer integration.
export {
ApolloServer,
registerServer,
ServerRegistration,
} from './ApolloServer';

View file

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"include": ["src/**/*"],
"exclude": ["**/__tests__", "**/__mocks__"],
"references": [
{ "path": "../apollo-server-core" },
]
}

View file

@ -19,6 +19,7 @@
{ "path": "./packages/apollo-server-core" },
{ "path": "./packages/apollo-server-errors" },
{ "path": "./packages/apollo-server-express" },
{ "path": "./packages/apollo-server-fastify" },
{ "path": "./packages/apollo-server-hapi" },
{ "path": "./packages/apollo-server-koa" },
{ "path": "./packages/apollo-server-lambda" },

View file

@ -16,6 +16,7 @@
{ "path": "./packages/apollo-server-cloud-functions/src/__tests__/" },
{ "path": "./packages/apollo-server-core/src/__tests__/" },
{ "path": "./packages/apollo-server-express/src/__tests__/" },
{ "path": "./packages/apollo-server-fastify/src/__tests__/" },
{ "path": "./packages/apollo-server-hapi/src/__tests__/" },
{ "path": "./packages/apollo-server-koa/src/__tests__/" },
{ "path": "./packages/apollo-server-lambda/src/__tests__/" },