Skip to content

Commit

Permalink
driver file
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo committed Mar 3, 2023
1 parent 6b5af54 commit 2f2989e
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 321 deletions.
319 changes: 319 additions & 0 deletions packages/nestjs/src/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import type {
Express,
Request as ExpressRequest,
Response as ExpressResponse,
} from 'express'
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import { printSchema } from 'graphql'
import { createYoga, YogaServerInstance, YogaServerOptions } from 'graphql-yoga'
import type { ExecutionParams } from 'subscriptions-transport-ws'
import { Injectable, Logger } from '@nestjs/common'
import {
AbstractGraphQLDriver,
GqlModuleOptions,
GqlSubscriptionService,
SubscriptionConfig,
} from '@nestjs/graphql'

export type YogaDriverPlatform = 'express' | 'fastify'

export type YogaDriverServerContext<Platform extends YogaDriverPlatform> =
Platform extends 'fastify'
? {
req: FastifyRequest
reply: FastifyReply
}
: {
req: ExpressRequest
res: ExpressResponse
}

export type YogaDriverServerOptions<Platform extends YogaDriverPlatform> = Omit<
YogaServerOptions<YogaDriverServerContext<Platform>, never>,
'context' | 'schema'
>

export type YogaDriverServerInstance<Platform extends YogaDriverPlatform> =
YogaServerInstance<YogaDriverServerContext<Platform>, never>

export type YogaDriverConfig<Platform extends YogaDriverPlatform = 'express'> =
GqlModuleOptions &
YogaDriverServerOptions<Platform> & {
/**
* Subscriptions configuration. Passing `true` will install only `graphql-ws`.
*/
subscriptions?: boolean | YogaDriverSubscriptionConfig
}

export type YogaDriverSubscriptionConfig = {
'graphql-ws'?: Omit<SubscriptionConfig['graphql-ws'], 'onSubscribe'>
'subscriptions-transport-ws'?: Omit<
SubscriptionConfig['subscriptions-transport-ws'],
'onOperation'
>
}

export abstract class AbstractYogaDriver<
Platform extends YogaDriverPlatform,
> extends AbstractGraphQLDriver<YogaDriverConfig<Platform>> {
protected yoga!: YogaDriverServerInstance<Platform>

public async start(options: YogaDriverConfig<Platform>) {
const platformName = this.httpAdapterHost.httpAdapter.getType() as Platform
options = {
...options,
// disable error masking by default
maskedErrors: options.maskedErrors == null ? false : options.maskedErrors,
// disable graphiql in production
graphiql:
options.graphiql == null
? process.env.NODE_ENV !== 'production'
: options.graphiql,
}
if (platformName === 'express') {
return this.registerExpress(options as YogaDriverConfig<'express'>)
}
if (platformName === 'fastify') {
return this.registerFastify(options as YogaDriverConfig<'fastify'>)
}
throw new Error(`Provided HttpAdapter "${platformName}" not supported`)
}

public async stop() {
// noop
}

protected registerExpress(
options: YogaDriverConfig<'express'>,
{ preStartHook }: { preStartHook?: (app: Express) => void } = {},
) {
const app: Express = this.httpAdapterHost.httpAdapter.getInstance()

preStartHook?.(app)

// nest's logger doesnt have the info method
class LoggerWithInfo extends Logger {
constructor(context: string) {
super(context)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info(message: any, ...args: any[]) {
this.log(message, ...args)
}
}

const yoga = createYoga<YogaDriverServerContext<'express'>>({
...options,
graphqlEndpoint: options.path,
// disable logging by default
// however, if `true` use nest logger
logging:
options.logging == null
? false
: options.logging
? new LoggerWithInfo('YogaDriver')
: options.logging,
})

this.yoga = yoga as YogaDriverServerInstance<Platform>

app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res }))
}

protected registerFastify(
options: YogaDriverConfig<'fastify'>,
{ preStartHook }: { preStartHook?: (app: FastifyInstance) => void } = {},
) {
const app: FastifyInstance = this.httpAdapterHost.httpAdapter.getInstance()

preStartHook?.(app)

const yoga = createYoga<YogaDriverServerContext<'fastify'>>({
...options,
graphqlEndpoint: options.path,
// disable logging by default
// however, if `true` use fastify logger
logging:
options.logging == null
? false
: options.logging
? app.log
: options.logging,
})

this.yoga = yoga as YogaDriverServerInstance<Platform>

app.all(yoga.graphqlEndpoint, async (req, reply) => {
const response = await yoga.handleNodeRequest(req, {
req,
reply,
})
response.headers.forEach((value, key) => reply.header(key, value))
reply.status(response.status)
reply.send(response.body)
return reply
})
}
}

@Injectable()
export class YogaDriver<
Platform extends YogaDriverPlatform = 'express',
> extends AbstractYogaDriver<Platform> {
private subscriptionService?: GqlSubscriptionService

public async start(options: YogaDriverConfig<Platform>) {
const opts = await this.graphQlFactory.mergeWithSchema<
YogaDriverConfig<Platform>
>(options)

if (opts.definitions?.path) {
if (!opts.schema) {
throw new Error('Schema is required when generating definitions')
}
await this.graphQlFactory.generateDefinitions(
printSchema(opts.schema),
opts,
)
}

await super.start(opts)

if (opts.subscriptions) {
if (!opts.schema) {
throw new Error('Schema is required when using subscriptions')
}

const config: SubscriptionConfig =
opts.subscriptions === true
? {
'graphql-ws': true,
}
: opts.subscriptions

if (config['graphql-ws']) {
config['graphql-ws'] =
typeof config['graphql-ws'] === 'object' ? config['graphql-ws'] : {}

config['graphql-ws'].onSubscribe = async (ctx, msg) => {
const {
schema,
execute,
subscribe,
contextFactory,
parse,
validate,
} = this.yoga.getEnveloped({
...ctx,
// @ts-expect-error context extra is from graphql-ws/lib/use/ws
req: ctx.extra.request,
// @ts-expect-error context extra is from graphql-ws/lib/use/ws
socket: ctx.extra.socket,
params: msg.payload,
})

const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory({ execute, subscribe }),
}

const errors = validate(args.schema, args.document)
if (errors.length) return errors
return args
}
}

if (config['subscriptions-transport-ws']) {
config['subscriptions-transport-ws'] =
typeof config['subscriptions-transport-ws'] === 'object'
? config['subscriptions-transport-ws']
: {}

config['subscriptions-transport-ws'].onOperation = async (
_msg: unknown,
params: ExecutionParams,
ws: WebSocket,
) => {
const {
schema,
execute,
subscribe,
contextFactory,
parse,
validate,
} = this.yoga.getEnveloped({
...params.context,
req:
// @ts-expect-error upgradeReq does exist but is untyped
ws.upgradeReq,
socket: ws,
params,
})

const args = {
schema,
operationName: params.operationName,
document:
typeof params.query === 'string'
? parse(params.query)
: params.query,
variables: params.variables,
context: await contextFactory({ execute, subscribe }),
}

const errors = validate(args.schema, args.document)
if (errors.length) return errors
return args
}
}

this.subscriptionService = new GqlSubscriptionService(
{
schema: opts.schema,
path: opts.path,
execute: (...args) => {
const contextValue =
args[0].contextValue ||
// @ts-expect-error args can be inlined with graphql-js@<=15
args[3]
if (!contextValue) {
throw new Error(
'Execution arguments are missing the context value',
)
}
return (
contextValue
// @ts-expect-error execute method will be available, see above
.execute(...args)
)
},
subscribe: (...args) => {
const contextValue =
args[0].contextValue ||
// @ts-expect-error args can be inlined with graphql-js@<=15
args?.[3]
if (!contextValue) {
throw new Error(
'Subscribe arguments are missing the context value',
)
}
return (
contextValue
// @ts-expect-error execute method will be available, see above
.subscribe(...args)
)
},
...config,
},
this.httpAdapterHost.httpAdapter.getHttpServer(),
)
}
}

public async stop() {
await this.subscriptionService?.stop()
}
}
2 changes: 1 addition & 1 deletion packages/nestjs/src/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
YogaDriver,
YogaDriverConfig,
YogaDriverPlatform,
} from './index.js'
} from './driver.js'

export type YogaFederationDriverConfig<
Platform extends YogaDriverPlatform = 'express',
Expand Down
Loading

0 comments on commit 2f2989e

Please sign in to comment.