mirror of
https://github.com/vale981/apollo-server
synced 2025-03-04 17:21:42 -05:00
feat(fastify) Apollo Fastify server integration resolve #626
This commit is contained in:
parent
ccd974dbc6
commit
bacdeae80d
17 changed files with 1411 additions and 0 deletions
|
@ -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
|
||||
|
||||
|
|
23
README.md
23
README.md
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
6
packages/apollo-server-fastify/.npmignore
Normal file
6
packages/apollo-server-fastify/.npmignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
*
|
||||
!src/**/*
|
||||
!dist/**/*
|
||||
dist/**/*.test.*
|
||||
!package.json
|
||||
!README.md
|
45
packages/apollo-server-fastify/README.md
Normal file
45
packages/apollo-server-fastify/README.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: Fastify
|
||||
description: Setting up Apollo Server with Fastify
|
||||
---
|
||||
|
||||
[](https://badge.fury.io/js/apollo-server-fastify) [](https://circleci.com/gh/apollographql/apollo-server) [](https://coveralls.io/github/apollographql/apollo-server?branch=master) [](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!
|
3
packages/apollo-server-fastify/jest.config.js
Normal file
3
packages/apollo-server-fastify/jest.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
const config = require('../../jest.config.base');
|
||||
|
||||
module.exports = Object.assign(Object.create(null), config);
|
42
packages/apollo-server-fastify/package.json
Normal file
42
packages/apollo-server-fastify/package.json
Normal 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"
|
||||
}
|
||||
}
|
145
packages/apollo-server-fastify/src/ApolloServer.ts
Normal file
145
packages/apollo-server-fastify/src/ApolloServer.ts
Normal 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',
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
143
packages/apollo-server-fastify/src/__tests__/datasource.test.ts
Normal file
143
packages/apollo-server-fastify/src/__tests__/datasource.test.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.test.base",
|
||||
"include": ["**/*"],
|
||||
"references": [
|
||||
{ "path": "../../" },
|
||||
{ "path": "../../../apollo-server-integration-testsuite" }
|
||||
]
|
||||
}
|
77
packages/apollo-server-fastify/src/fastifyApollo.ts
Normal file
77
packages/apollo-server-fastify/src/fastifyApollo.ts
Normal 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,
|
||||
});
|
||||
}
|
29
packages/apollo-server-fastify/src/index.ts
Normal file
29
packages/apollo-server-fastify/src/index.ts
Normal 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';
|
12
packages/apollo-server-fastify/tsconfig.json
Normal file
12
packages/apollo-server-fastify/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/__tests__", "**/__mocks__"],
|
||||
"references": [
|
||||
{ "path": "../apollo-server-core" },
|
||||
]
|
||||
}
|
|
@ -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" },
|
||||
|
|
|
@ -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__/" },
|
||||
|
|
Loading…
Add table
Reference in a new issue