Skip to content

Instantly share code, notes, and snippets.

@ronny
Last active November 17, 2022 00:03
Show Gist options
  • Save ronny/3f4306d99aca2ace971905a8e54e075d to your computer and use it in GitHub Desktop.
Save ronny/3f4306d99aca2ace971905a8e54e075d to your computer and use it in GitHub Desktop.
Apollo Server v4 integration for Cloudflare Workers
/*
Context:
- https://github.com/apollographql/apollo-server/issues/6034
- https://github.com/apollographql/apollo-server/blob/version-3/packages/apollo-server-cloudflare/src/ApolloServer.ts
- https://github.com/cloudflare/workers-graphql-server/blob/master/src/index.js
This is largely based on the Lambda integration:
https://github.com/apollo-server-integrations/apollo-server-integration-aws-lambda/blob/main/src/index.ts
This is partially working (as of 12 Nov 2022), except for sending schema and reporting metrics to Apollo
(it uses many Node specific things which don't exist in Cloudflare Workers), BUT you'd need to enable
workers node polyfill (see https://developers.cloudflare.com/workers/wrangler/configuration/#node-compatibility ;
basically you need `node_compat = true` in `wrangler.toml`) and also monkeypatch a couple of files in `@apollo/server`:
```
✘ [ERROR] No matching export in "node-modules-polyfills:util" for import "promisify"
node_modules/@apollo/server/dist/esm/plugin/usageReporting/plugin.js:8:9:
8 │ import { promisify } from 'util';
╵ ~~~~~~~~~
✘ [ERROR] No matching export in "node-modules-polyfills:url" for import "URLSearchParams"
node_modules/@apollo/server/dist/esm/runHttpQuery.js:4:9:
4 │ import { URLSearchParams } from 'url';
╵ ~~~~~~~~~~~~~~~
```
In `runHttpQuery.js` you can just delete the whole import line, since `URLSearchParams` is already part
of the Web API which is already supported by Workers, so no need to import it from node's `url` module.
In the usage reporting plugin file, find this line:
```js
const compressed = await gzipPromise(message);
```
and replace with:
```js
const compressed = await new Promise((resolve, reject) => gzip(message, (error, foo) => error ? reject(error) : resolve(foo)));
```
then delete these lines:
```js
import { promisify } from 'util';
...
const gzipPromise = promisify(gzip);
```
These still won't make the reporting to Apollo work, it's just enough to get Wrangler to be able to package everything
and run it in Workers.
*/
import {
ApolloServer,
BaseContext,
ContextFunction,
HeaderMap,
HTTPGraphQLRequest,
} from "@apollo/server";
export interface CloudflareWorkersContextFunctionArgument<TEnv> {
request: Request;
env: TEnv;
ctx: ExecutionContext;
}
export interface CloudflareWorkersHandlerOptions<
TEnv,
TContext extends BaseContext
> {
path: string;
context?: ContextFunction<
[CloudflareWorkersContextFunctionArgument<TEnv>],
TContext
>;
}
export function startServerAndCreateCloudflareWorkersHandler<TEnv>(
server: ApolloServer<BaseContext>,
options: CloudflareWorkersHandlerOptions<TEnv, BaseContext>
): ExportedHandlerFetchHandler<TEnv>;
export function startServerAndCreateCloudflareWorkersHandler<
TEnv,
TContext extends BaseContext
>(
server: ApolloServer<TContext>,
options: CloudflareWorkersHandlerOptions<TEnv, TContext>
): ExportedHandlerFetchHandler<TEnv> {
server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests();
const defaultContext: ContextFunction<
[CloudflareWorkersContextFunctionArgument<TEnv>],
// This `any` is safe because the overload above shows that context can
// only be left out if you're using BaseContext as your context, and {} is a
// valid BaseContext.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
> = async () => ({});
const contextFunction: ContextFunction<
[CloudflareWorkersContextFunctionArgument<TEnv>],
TContext
> = options?.context ?? defaultContext;
return async function apolloServerIntegrationCloudflareWorkersHandler(
request: Request,
env: TEnv,
ctx: ExecutionContext
): Promise<Response> {
const requestUrl = new URL(request.url);
if (requestUrl.pathname !== options.path || request.method !== "POST") {
return new Response("Not found", { status: 404 });
}
try {
const { body, headers, status } = await server.executeHTTPGraphQLRequest({
httpGraphQLRequest: await asHTTPGraphQLRequest(request),
context: () => contextFunction({ request, env, ctx }),
});
if (body.kind === "chunked") {
// TODO: check if this is possible in Workers, then implement it
throw new Error("Incremental delivery not implemented");
}
return new Response(body.string, {
status,
headers: {
...Object.fromEntries(headers),
"content-length": Buffer.byteLength(body.string).toString(),
},
});
} catch (err) {
const error = err instanceof Error ? err : new Error(`${err}`); // TODO: do it betterer? 🤷‍♂️
return new Response(error.message, { status: 500 });
}
};
}
/** converts a Cloudflare Workers Request to an Apollo Server HTTPGraphQLRequest */
async function asHTTPGraphQLRequest(
request: Request
): Promise<HTTPGraphQLRequest> {
const reqUrl = new URL(request.url);
const headers = new HeaderMap();
request.headers.forEach((value, key) => headers.set(key, value));
return {
method: request.method,
search: reqUrl.search,
headers,
body: await request.json(),
};
}
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateCloudflareWorkersHandler } from "./apollo-server-integration-cloudflare-workers.js";
export interface Env {
// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
// MY_KV_NAMESPACE: KVNamespace;
//
// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
// MY_DURABLE_OBJECT: DurableObjectNamespace;
//
// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
// MY_BUCKET: R2Bucket;
}
const typeDefs = `#graphql
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => "world",
},
};
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
});
// eslint-disable-next-line import/no-default-export
export default {
// This is just a proof of concept. After further thinking, if we want to implement the integration for real
// I think we can do a more flexible integration that doesn't take over the whole handler function.
fetch: startServerAndCreateCloudflareWorkersHandler<Env>(apolloServer, {
path: "/graphql",
}),
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment