@@ -5,11 +5,15 @@ import {
5
5
type GqlModuleOptions ,
6
6
} from '@nestjs/graphql' ;
7
7
import type { RouteOptions as FastifyRoute } from 'fastify' ;
8
+ import type { ExecutionArgs } from 'graphql' ;
9
+ import { makeHandler as makeGqlWSHandler } from 'graphql-ws/use/@fastify/websocket' ;
8
10
import {
9
11
createYoga ,
12
+ type envelop ,
10
13
type YogaServerInstance ,
11
14
type YogaServerOptions ,
12
15
} from 'graphql-yoga' ;
16
+ import type { WebSocket } from 'ws' ;
13
17
import { type GqlContextType } from '~/common' ;
14
18
import { HttpAdapter , type IRequest } from '../http' ;
15
19
import { type IResponse } from '../http/types' ;
@@ -55,7 +59,14 @@ export class Driver extends AbstractDriver<DriverConfig> {
55
59
} ) ;
56
60
57
61
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' ] ,
59
70
url : this . yoga . graphqlEndpoint ,
60
71
handler : this . httpHandler ,
61
72
} ) ;
@@ -77,6 +88,82 @@ export class Driver extends AbstractDriver<DriverConfig> {
77
88
. send ( res . body ) ;
78
89
} ;
79
90
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
+
80
167
async stop ( ) {
81
168
// noop
82
169
}
0 commit comments