Skip to content

Commit 9280a6b

Browse files
committed
Setup graphql-ws
1 parent b9ab640 commit 9280a6b

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"graphql": "^16.9.0",
7878
"graphql-parse-resolve-info": "^4.14.0",
7979
"graphql-scalars": "^1.22.4",
80+
"graphql-ws": "^6.0.4",
8081
"graphql-yoga": "^5.13.4",
8182
"human-format": "^1.2.0",
8283
"image-size": "^2.0.2",

src/core/graphql/driver.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ import {
55
type GqlModuleOptions,
66
} from '@nestjs/graphql';
77
import type { RouteOptions as FastifyRoute } from 'fastify';
8+
import type { ExecutionArgs } from 'graphql';
9+
import { makeHandler as makeGqlWSHandler } from 'graphql-ws/use/@fastify/websocket';
810
import {
911
createYoga,
12+
type envelop,
1013
type YogaServerInstance,
1114
type YogaServerOptions,
1215
} from 'graphql-yoga';
16+
import type { WebSocket } from 'ws';
1317
import { type GqlContextType } from '~/common';
1418
import { HttpAdapter, type IRequest } from '../http';
1519
import { type IResponse } from '../http/types';
@@ -55,7 +59,14 @@ export class Driver extends AbstractDriver<DriverConfig> {
5559
});
5660

5761
fastify.route({
58-
method: ['GET', 'POST', 'OPTIONS'],
62+
method: 'GET',
63+
url: this.yoga.graphqlEndpoint,
64+
handler: this.httpHandler,
65+
// Allow this same path to handle websocket upgrade requests.
66+
wsHandler: this.makeWsHandler(options),
67+
});
68+
fastify.route({
69+
method: ['POST', 'OPTIONS'],
5970
url: this.yoga.graphqlEndpoint,
6071
handler: this.httpHandler,
6172
});
@@ -77,6 +88,82 @@ export class Driver extends AbstractDriver<DriverConfig> {
7788
.send(res.body);
7889
};
7990

91+
/**
92+
* This code ties fastify, yoga, and graphql-ws together.
93+
* Execution layers in order:
94+
* 1. fastify route (http path matching)
95+
* 2. fastify's websocket plugin (http upgrade request & websocket open/close)
96+
* This allows our fastify hooks to be executed.
97+
* And provides a consistent Fastify `Request` type,
98+
* instead of a raw `IncomingMessage`.
99+
* 3. `graphql-ws`'s fastify handler (adapts #2 to graphql-ws)
100+
* 4. `graphql-ws` (handles specific gql protocol over websockets)
101+
* 5. `graphql-yoga` is unwrapped to `envelop`.
102+
* Yoga just wraps `envelop` and handles more of the http layer.
103+
* We really just reference `envelop` hooks with our "Yoga Plugins".
104+
* So this allows our "yoga" plugins to be executed.
105+
*/
106+
private makeWsHandler(options: DriverConfig) {
107+
interface WsExecutionArgs extends ExecutionArgs {
108+
envelop: ReturnType<ReturnType<typeof envelop>>;
109+
}
110+
111+
// The graphql-ws handler which accepts the fastify websocket/request and
112+
// orchestrates the subscription setup & execution.
113+
// This forwards to yoga/envelop.
114+
// This was adapted from yoga's graphql-ws example.
115+
// https://github.com/dotansimha/graphql-yoga/tree/main/examples/graphql-ws
116+
const wsHandler = makeGqlWSHandler<
117+
Record<string, unknown>,
118+
{ socket: WebSocket; request: IRequest }
119+
>({
120+
schema: options.schema!,
121+
// Custom execute/subscribe functions that really just defer to a
122+
// unique envelop (yoga) instance per request.
123+
execute: (wsArgs) => {
124+
const { envelop, ...args } = wsArgs as WsExecutionArgs;
125+
return envelop.execute(args);
126+
},
127+
subscribe: (wsArgs) => {
128+
const { envelop, ...args } = wsArgs as WsExecutionArgs;
129+
return envelop.subscribe(args);
130+
},
131+
// Create a unique envelop/yoga instance for each subscription.
132+
// This allows "yoga" plugins that are really just envelop hooks
133+
// to be executed.
134+
onSubscribe: async (ctx, id, payload) => {
135+
const {
136+
extra: { request, socket },
137+
} = ctx;
138+
const envelop = this.yoga.getEnveloped({
139+
req: request,
140+
socket,
141+
params: payload, // Same(ish?) shape as YogaInitialContext.params
142+
});
143+
144+
const args: WsExecutionArgs = {
145+
schema: envelop.schema,
146+
operationName: payload.operationName,
147+
document: envelop.parse(payload.query),
148+
variableValues: payload.variables,
149+
contextValue: await envelop.contextFactory(),
150+
// These are needed in our execute()/subscribe() declared above.
151+
// Public examples put these functions in the context, but I don't
152+
// like exposing that implementation detail to the rest of the app.
153+
envelop,
154+
};
155+
156+
const errors = envelop.validate(args.schema, args.document);
157+
if (errors.length) {
158+
return errors;
159+
}
160+
return args;
161+
},
162+
});
163+
164+
return wsHandler;
165+
}
166+
80167
async stop() {
81168
// noop
82169
}

yarn.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5893,6 +5893,7 @@ __metadata:
58935893
graphql: "npm:^16.9.0"
58945894
graphql-parse-resolve-info: "npm:^4.14.0"
58955895
graphql-scalars: "npm:^1.22.4"
5896+
graphql-ws: "npm:^6.0.4"
58965897
graphql-yoga: "npm:^5.13.4"
58975898
human-format: "npm:^1.2.0"
58985899
husky: "npm:^4.3.8"
@@ -8091,7 +8092,7 @@ __metadata:
80918092
languageName: node
80928093
linkType: hard
80938094

8094-
"graphql-ws@npm:6.0.4, graphql-ws@npm:^6.0.3":
8095+
"graphql-ws@npm:6.0.4, graphql-ws@npm:^6.0.3, graphql-ws@npm:^6.0.4":
80958096
version: 6.0.4
80968097
resolution: "graphql-ws@npm:6.0.4"
80978098
peerDependencies:

0 commit comments

Comments
 (0)