Skip to content

Commit

Permalink
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 43 deletions.
102 changes: 102 additions & 0 deletions apollo/apollo-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { ServerResponse } from 'http'
import type { LandingPage } from 'apollo-server-plugin-base'
import {
useBody,
useQuery,
IncomingMessage,
CompatibilityEvent,
EventHandler,
} from 'h3'
import type { GraphQLOptions } from 'apollo-server-core'
import {
ApolloServerBase,
convertNodeHttpToRequest,
runHttpQuery,
isHttpQueryError,
} from 'apollo-server-core'

export interface ServerRegistration {
path?: string
disableHealthCheck?: boolean
onHealthCheck?: (event: CompatibilityEvent) => Promise<any>
}

// Originally taken from https://github.com/newbeea/nuxt3-apollo-starter/blob/master/server/graphql/apollo-server.ts
// TODO: Implement health check https://github.com/apollographql/apollo-server/blob/main/docs/source/monitoring/health-checks.md
export class ApolloServer extends ApolloServerBase {
async createGraphQLServerOptions(
request?: IncomingMessage,
reply?: ServerResponse
): Promise<GraphQLOptions> {
return this.graphQLServerOptions({ request, reply })
}

createHandler({
path,
disableHealthCheck,
onHealthCheck,
}: ServerRegistration = {}): EventHandler {
this.graphqlPath = path || '/graphql'
const landingPage = this.getLandingPage()

return async (event: CompatibilityEvent) => {
const options = await this.createGraphQLServerOptions(
event.req,
event.res
)
try {
if (landingPage) {
const landingPageHtml = this.handleLandingPage(event, landingPage)
if (landingPageHtml) {
return landingPageHtml
}
}

const { graphqlResponse, responseInit } = await runHttpQuery([], {
method: event.req.method || 'GET',
options,
query:
event.req.method === 'POST'
? await useBody(event)
: useQuery(event),
request: convertNodeHttpToRequest(event.req),
})
if (responseInit.headers) {
for (const [name, value] of Object.entries<string>(
responseInit.headers
))
event.res.setHeader(name, value)
}
event.res.statusCode = responseInit.status || 200
return graphqlResponse
} catch (error: any) {
if (!isHttpQueryError(error)) {
throw error
}

if (error.headers) {
for (const [name, value] of Object.entries<string>(error.headers))
event.res.setHeader(name, value)
}
event.res.statusCode = error.statusCode || 500
return error.message
}
}
}

private handleLandingPage(
event: CompatibilityEvent,
landingPage: LandingPage
): string | undefined {
const url = event.req.url?.split('?')[0]
if (event.req.method === 'GET' && url === this.graphqlPath) {
const prefersHtml = event.req.headers.accept?.includes('text/html')

if (prefersHtml) {
return landingPage.html
}
}
}
}

export default ApolloServer
113 changes: 72 additions & 41 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import http from 'http'
import express from 'express'
import { ApolloServer } from 'apollo-server-express'
import 'reflect-metadata' // Needed for tsyringe
import {
ApolloServerPluginDrainHttpServer,
ApolloServerPluginLandingPageLocalDefault,
} from 'apollo-server-core'
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache'
import { Environment } from '../config'
import { createApp } from 'h3'
import { configure as configureTsyringe } from './tsyringe.config'
import { buildContext } from './context'
import { loadSchemaWithResolvers } from './schema'
import { resolve } from './tsyringe'
import { ApolloServer } from '~/apollo/apollo-server'

// Workaround for issue with Azure deploy: https://github.com/unjs/nitro/issues/351
// Original code taken from https://github.com/nodejs/node/blob/main/lib/_http_outgoing.js
Expand Down Expand Up @@ -44,46 +43,78 @@ http.OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
return this
}

// Create express instance
const app = express()
if (useRuntimeConfig().public.environment === Environment.Production) {
// Azure uses a reverse proxy, which changes some API values (notably express things it is not accessed through a secure https connection)
// So we need to adjust for this, see http://expressjs.com/en/guide/behind-proxies.html
app.set('trust proxy', 1)
// Workaround for issue with Azure deploy: https://github.com/unjs/nitro/issues/351
// Original code taken from https://github.com/nodejs/node/blob/main/lib/internal/streams/readable.js
http.IncomingMessage.Readable.prototype.unpipe = function (dest) {
// CHANGED: Add fallback if not existing
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// @ts-ignore: is workaround anyway
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const state = (this._readableState as any) || { pipes: [] }
const unpipeInfo = { hasUnpiped: false }

// If we're not piping anywhere, then do nothing.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (state.pipes.length === 0) return this

if (!dest) {
// remove all.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const dests = state.pipes as any[]
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
state.pipes = []
this.pause()

for (let i = 0; i < dests.length; i++)
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
dests[i].emit('unpipe', this, { hasUnpiped: false })
return this
}

// Try to find the right one.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const index = state.pipes.indexOf(dest)
if (index === -1) return this

// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
state.pipes.splice(index, 1)
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (state.pipes.length === 0) this.pause()

dest.emit('unpipe', this, unpipeInfo)

return this
}

const app = createApp()
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const httpServer = http.createServer(app)

// TODO: Replace this with await, once esbuild supports top-level await
void configureTsyringe()
.then(async () => {
const passportInitializer = resolve('PassportInitializer')
passportInitializer.initialize()
passportInitializer.install(app)

const server = new ApolloServer({
schema: await loadSchemaWithResolvers(),
context: buildContext,
introspection: true,
plugins: [
// Enable Apollo Studio in development, and also in production (at least for now)
ApolloServerPluginLandingPageLocalDefault({ footer: false }),
// Gracefully shutdown HTTP server when Apollo server terminates
ApolloServerPluginDrainHttpServer({ httpServer }),
],
// Only reply to requests with a Content-Type header to prevent CSRF and XS-Search attacks
// https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf
csrfPrevention: true,
cache: new InMemoryLRUCache(),
})

async function startServer() {
await server.start()
server.applyMiddleware({ app, path: '/' })
}
void startServer()
})
.catch((error) => {
console.error('Error while executing configureTsyringe', error)
export default defineLazyEventHandler(async () => {
await configureTsyringe()

const passportInitializer = resolve('PassportInitializer')
passportInitializer.initialize()
passportInitializer.install(app)

const server = new ApolloServer({
schema: await loadSchemaWithResolvers(),
context: buildContext,
introspection: true,
plugins: [
// Enable Apollo Studio in development, and also in production (at least for now)
ApolloServerPluginLandingPageLocalDefault({ footer: false }),
// Gracefully shutdown HTTP server when Apollo server terminates
ApolloServerPluginDrainHttpServer({ httpServer }),
],
// Only reply to requests with a Content-Type header to prevent CSRF and XS-Search attacks
// https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf
csrfPrevention: true,
cache: new InMemoryLRUCache(),
})

export default app
await server.start()
return server.createHandler({
path: '/api',
})
})
6 changes: 4 additions & 2 deletions server/user/passport-initializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import connectRedis from 'connect-redis'
import { Express } from 'express-serve-static-core'
import { App } from 'h3'
import session from 'express-session'
import passport from 'passport'
import { RedisClientType } from 'redis'
Expand All @@ -25,7 +25,7 @@ export default class PassportInitializer {
)
}

install(app: Express): void {
install(app: App): void {
const config = useRuntimeConfig()

// TODO: Use redis store also for development as soon as https://github.com/tj/connect-redis/issues/336 is fixed (and mock-redis is compatible with redis v4)
Expand All @@ -43,6 +43,7 @@ export default class PassportInitializer {
// Add middleware that sends and receives the session ID using cookies
// See https://github.com/expressjs/session#readme
app.use(
// @ts-ignore: https://github.com/unjs/h3/issues/146
session({
store,
// The secret used to sign the session cookie
Expand All @@ -64,6 +65,7 @@ export default class PassportInitializer {
})
)
// Add passport as middleware (this more or less only adds the _passport variable to the request)
// @ts-ignore: https://github.com/unjs/h3/issues/146
app.use(passport.initialize())
// Add middleware that authenticates request based on the current session state (i.e. we alter the request to contain the hydrated user object instead of only the session ID)
app.use(passport.session())
Expand Down

0 comments on commit 47ec3db

Please sign in to comment.