Skip to content

Commit 7b7a01d

Browse files
authored
Improve Fastify example (#6)
- Change plugin to be just a handler. Users get to install it where they want. Uses a simpler API. - Add drain plugin. - Fix build issue with exactOptionalPropertyTypes - Put assertStarted in correct place. - Decide that we did multiple header values correctly. - Set search correctly when path is non-trivial. - Upgrade integration suite to be more lax about certain errors; remove extra JSON-related code that was only there to make those tests pass. Paired with @trevor-scheer.
1 parent 49dccd6 commit 7b7a01d

File tree

6 files changed

+123
-122
lines changed

6 files changed

+123
-122
lines changed

package-lock.json

Lines changed: 24 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"packages/*"
2929
],
3030
"devDependencies": {
31-
"@apollo/server-integration-testsuite": "4.0.0-alpha.1",
31+
"@apollo/server-integration-testsuite": "4.0.0-alpha.2",
3232
"@apollo/utils.withrequired": "1.0.0",
3333
"@jest/types": "28.1.3",
3434
"@types/aws-lambda": "8.10.101",

packages/fastify/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"fastify-plugin": "^4.0.0"
2727
},
2828
"peerDependencies": {
29-
"@apollo/server": "^4.0.0-alpha.1",
29+
"@apollo/server": "^4.0.0-alpha.2",
3030
"fastify": "^4.3.0",
3131
"graphql": "^16.5.0"
3232
}

packages/fastify/src/__tests__/integration.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@ import {
33
CreateServerForIntegrationTestsOptions,
44
defineIntegrationTestSuite,
55
} from "@apollo/server-integration-testsuite";
6-
import { fastifyPlugin } from "..";
6+
import { fastifyDrainPlugin, fastifyHandler } from "..";
77
import fastify from "fastify";
88

99
describe("fastifyPlugin", () => {
1010
defineIntegrationTestSuite(async function (
1111
serverOptions: ApolloServerOptions<BaseContext>,
1212
testOptions?: CreateServerForIntegrationTestsOptions,
1313
) {
14+
const app = fastify();
15+
1416
const server = new ApolloServer({
1517
...serverOptions,
16-
plugins: [...(serverOptions.plugins ?? [])],
18+
plugins: [...(serverOptions.plugins ?? []), fastifyDrainPlugin(app)],
1719
});
1820

19-
const app = fastify();
20-
2121
await server.start();
22-
app.register(fastifyPlugin(server, testOptions));
22+
23+
app.route({
24+
// Note: we register for HEAD mostly because the integration test suite
25+
// ensures that our middleware appropriate rejects such requests. In your
26+
// app, you would only want to register for GET and POST.
27+
method: ["GET", "POST", "HEAD"],
28+
url: "/",
29+
handler: fastifyHandler(server, { context: testOptions?.context }),
30+
});
2331

2432
const url = await app.listen({ port: 0 });
2533

packages/fastify/src/index.ts

Lines changed: 83 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,116 @@
11
import type {
22
ApolloServer,
3+
ApolloServerPlugin,
34
BaseContext,
45
ContextFunction,
56
HTTPGraphQLRequest,
67
} from "@apollo/server";
78
import type { WithRequired } from "@apollo/utils.withrequired";
89
import type {
9-
FastifyPluginCallback,
10+
FastifyInstance,
1011
FastifyReply,
1112
FastifyRequest,
13+
RouteHandlerMethod,
1214
} from "fastify";
13-
import type { PluginMetadata } from "fastify-plugin";
14-
import fp from "fastify-plugin";
15-
16-
const pluginMetadata: PluginMetadata = {
17-
fastify: "4.x",
18-
name: "apollo-server-integration-fastify",
19-
};
15+
import { parse as urlParse } from "url";
2016

2117
export interface FastifyContextFunctionArgument {
2218
request: FastifyRequest;
2319
reply: FastifyReply;
2420
}
2521

26-
export interface LambdaHandlerOptions<TContext extends BaseContext> {
27-
context?: ContextFunction<[FastifyContextFunctionArgument], TContext>;
28-
}
29-
30-
export function fastifyPlugin(
31-
server: ApolloServer<BaseContext>,
32-
options?: LambdaHandlerOptions<BaseContext>,
33-
): FastifyPluginCallback;
34-
export function fastifyPlugin<TContext extends BaseContext>(
35-
server: ApolloServer<TContext>,
36-
options: WithRequired<LambdaHandlerOptions<TContext>, "context">,
37-
): FastifyPluginCallback;
38-
export function fastifyPlugin(
39-
server: ApolloServer<BaseContext>,
40-
options?: LambdaHandlerOptions<BaseContext>,
41-
) {
42-
return fp(fastifyHandler(server, options), pluginMetadata);
22+
export interface FastifyHandlerOptions<TContext extends BaseContext> {
23+
context?:
24+
| ContextFunction<[FastifyContextFunctionArgument], TContext>
25+
| undefined;
4326
}
4427

45-
function fastifyHandler(
28+
export function fastifyHandler(
4629
server: ApolloServer<BaseContext>,
47-
options?: LambdaHandlerOptions<BaseContext>,
48-
): FastifyPluginCallback;
49-
function fastifyHandler<TContext extends BaseContext>(
30+
options?: FastifyHandlerOptions<BaseContext>,
31+
): RouteHandlerMethod;
32+
export function fastifyHandler<TContext extends BaseContext>(
5033
server: ApolloServer<TContext>,
51-
options: WithRequired<LambdaHandlerOptions<TContext>, "context">,
52-
): FastifyPluginCallback;
53-
function fastifyHandler<TContext extends BaseContext>(
34+
options: WithRequired<FastifyHandlerOptions<TContext>, "context">,
35+
): RouteHandlerMethod;
36+
export function fastifyHandler<TContext extends BaseContext>(
5437
server: ApolloServer<TContext>,
55-
options?: LambdaHandlerOptions<TContext>,
56-
): FastifyPluginCallback {
57-
return async (fastify) => {
58-
server.assertStarted("fastifyHandler()");
59-
60-
// This `any` is safe because the overload above shows that context can
61-
// only be left out if you're using BaseContext as your context, and {} is a
62-
// valid BaseContext.
63-
const defaultContext: ContextFunction<
64-
[FastifyContextFunctionArgument],
65-
any
66-
> = async () => ({});
38+
options?: FastifyHandlerOptions<TContext>,
39+
): RouteHandlerMethod {
40+
server.assertStarted("fastifyHandler()");
6741

68-
const contextFunction: ContextFunction<
69-
[FastifyContextFunctionArgument],
70-
TContext
71-
> = options?.context ?? defaultContext;
42+
// This `any` is safe because the overload above shows that context can
43+
// only be left out if you're using BaseContext as your context, and {} is a
44+
// valid BaseContext.
45+
const defaultContext: ContextFunction<
46+
[FastifyContextFunctionArgument],
47+
any
48+
> = async () => ({});
7249

73-
fastify.removeContentTypeParser(["application/json"]);
74-
fastify.addContentTypeParser(
75-
"application/json",
76-
{ parseAs: "string" },
77-
async (_request, body, _done) => {
78-
try {
79-
return JSON.parse(body as string);
80-
} catch (err) {
81-
if ((body as string).trim() === "") {
82-
return {};
83-
}
84-
(err as any).statusCode = 400;
85-
throw err;
86-
}
87-
},
88-
);
89-
90-
// This is dumb but it gets the integration testsuite passing. We should
91-
// maybe consider relaxing some of the tests in order to accommodate server
92-
// frameworks that behave well or better than Apollo Server.
93-
fastify.addContentTypeParser("*", (_, __, done) => {
94-
done(null, null);
95-
});
50+
const contextFunction: ContextFunction<
51+
[FastifyContextFunctionArgument],
52+
TContext
53+
> = options?.context ?? defaultContext;
9654

97-
fastify.addHook("preHandler", async (request, reply) => {
98-
const headers = new Map<string, string>();
99-
for (const [key, value] of Object.entries(request.headers)) {
100-
// TODO: how does fastify handle duplicate headers?
101-
if (value !== undefined) {
102-
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
103-
}
55+
return async (request, reply) => {
56+
const headers = new Map<string, string>();
57+
for (const [key, value] of Object.entries(request.headers)) {
58+
if (value !== undefined) {
59+
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
10460
}
61+
}
10562

106-
const httpGraphQLRequest: HTTPGraphQLRequest = {
107-
method: request.method,
108-
headers,
109-
search:
110-
typeof request.raw.url === "string"
111-
? request.raw.url.substring(1)
112-
: "",
113-
body: request.body,
114-
};
63+
const httpGraphQLRequest: HTTPGraphQLRequest = {
64+
method: request.method,
65+
headers,
66+
search: urlParse(request.url).search ?? "",
67+
body: request.body,
68+
};
11569

116-
const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
117-
httpGraphQLRequest,
118-
context: () => contextFunction({ request, reply }),
119-
});
70+
const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
71+
httpGraphQLRequest,
72+
context: () => contextFunction({ request, reply }),
73+
});
12074

121-
if (httpGraphQLResponse.completeBody === null) {
122-
throw Error("Incremental delivery not implemented");
123-
}
75+
if (httpGraphQLResponse.completeBody === null) {
76+
throw Error("Incremental delivery not implemented");
77+
}
12478

125-
reply.code(httpGraphQLResponse.statusCode ?? 200);
126-
reply.headers(Object.fromEntries(httpGraphQLResponse.headers));
127-
reply.send(httpGraphQLResponse.completeBody);
79+
reply.code(httpGraphQLResponse.statusCode ?? 200);
80+
reply.headers(Object.fromEntries(httpGraphQLResponse.headers));
81+
reply.send(httpGraphQLResponse.completeBody);
12882

129-
return reply;
130-
});
83+
return reply;
84+
};
85+
}
86+
87+
// Add this plugin to your ApolloServer to drain the server during shutdown.
88+
// This works best with Node 18.2.0 or newer; with that version, Fastify will
89+
// use the new server.closeIdleConnections() to close idle connections, and the
90+
// plugin will close any other connections 10 seconds later. (With older Node,
91+
// the drain phase will hang until all connections naturally close; you can also
92+
// call `fastify({forceCloseConnections: true})` to make all connections immediately
93+
// close without grace.)
94+
export function fastifyDrainPlugin<TContext extends BaseContext>(
95+
app: FastifyInstance,
96+
): ApolloServerPlugin<TContext> {
97+
return {
98+
async serverWillStart() {
99+
return {
100+
async drainServer() {
101+
let timeout;
102+
if ("closeAllConnections" in app.server) {
103+
timeout = setTimeout(
104+
() => (app.server as any).closeAllConnections(),
105+
10_000,
106+
);
107+
}
108+
await app.close();
109+
if (timeout) {
110+
clearTimeout(timeout);
111+
}
112+
},
113+
};
114+
},
131115
};
132116
}

0 commit comments

Comments
 (0)