Skip to content

Commit

Permalink
Refactor endpoint handling (#1455)
Browse files Browse the repository at this point in the history
* chore(deps): update dependency vite to v3 (master) (#1444)

* chore(deps): update actions/checkout action to v3 (#1431)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency ioredis to v5.2.2 (#1450)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Replace cross-undici-fetch with @whatwg-node/fetch

* chore(deps): update dependency vite to v3

* Fix GraphiQL build

* Go

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>

* Check Endpoint Plugin

* Fix for new v3 API

* Update docs

* ..

* Add an ugly 404 page

* Bump whatwg-node/server to make Express happy

* Fix CF E2E

* Fix AWS

* Fix tests

* Go

* Go

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and renovate[bot] committed Jul 27, 2022
1 parent f64f21f commit 94db79d
Show file tree
Hide file tree
Showing 19 changed files with 252 additions and 73 deletions.
6 changes: 3 additions & 3 deletions e2e/tests/aws-lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ export const awsLambdaDeployment: DeploymentConfiguration<{
const lambdaGw = new awsx.apigateway.API('api', {
routes: [
{
path: '/',
path: '/graphql',
method: 'GET',
eventHandler: func,
},
{
path: '/',
path: '/graphql',
method: 'POST',
eventHandler: func,
},
Expand All @@ -100,6 +100,6 @@ export const awsLambdaDeployment: DeploymentConfiguration<{
console.log(`ℹ️ AWS Lambda Function deployed to URL: ${functionUrl.value}`)
// DOTAN: This is a known issue at the moment, this seems to fail to serve GraphiQL but POST does work.
// await assertGraphiQL(functionUrl.value)
await assertQuery(functionUrl.value)
await assertQuery(functionUrl.value + '/graphql')
},
}
3 changes: 3 additions & 0 deletions examples/azure-function/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"build": "node build.js",
"check": "tsc --pretty --noEmit"
},
"dependencies": {
"graphql-yoga": "2.13.4"
},
"devDependencies": {
"@azure/functions": "3.2.0",
"esbuild": "0.14.50",
Expand Down
4 changes: 1 addition & 3 deletions examples/azure-function/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ const httpTrigger: AzureFunction = async function (
info: context.log.info,
warn: context.log.warn,
},
graphiql: {
endpoint: '/api/yoga',
},
graphqlEndpoint: '/api/yoga',
})
context.log('HTTP trigger function processed a request.')

Expand Down
2 changes: 1 addition & 1 deletion examples/express/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express from 'express'
import { buildApp } from './app.js'
import { buildApp } from './app'

const app = express()

Expand Down
4 changes: 3 additions & 1 deletion examples/nextjs/pages/api/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export const config = {
export default createYoga<{
req: NextApiRequest
res: NextApiResponse
}>()
}>({
graphqlEndpoint: '/api/graphql',
})
2 changes: 1 addition & 1 deletion examples/node-esm/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { createServer } from 'http'

const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server started on port 4000')
console.info('Server started on http://localhost:4000/graphql')
})
7 changes: 6 additions & 1 deletion examples/service-worker/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createYoga } from 'graphql-yoga'

const yoga = createYoga()
// We can define GraphQL Route dynamically using env vars.
declare var GRAPHQL_ROUTE: string

const yoga = createYoga({
graphqlEndpoint: GRAPHQL_ROUTE || '/graphql',
})

self.addEventListener('fetch', yoga)
2 changes: 1 addition & 1 deletion examples/sveltekit/src/routes/api/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const yogaApp = createYoga<RequestEvent>({
useGraphQlJit()
// other plugins: https://www.envelop.dev/plugins
],
graphqlEndpoint: '/api/graphql',
graphiql: {
endpoint: '/api/graphql',
defaultQuery: `query Hello {
hello
}`
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-link/test/apollo-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Yoga Apollo Link', () => {
const endpoint = '/graphql'
const hostname = '127.0.0.1'
const yoga = createYoga({
endpoint,
graphqlEndpoint: endpoint,
logging: false,
maskedErrors: false,
schema: {
Expand Down
37 changes: 34 additions & 3 deletions packages/graphql-yoga/__tests__/node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ it('validation error is sent to clients', async () => {

describe('Requests', () => {
const endpoint = '/test-graphql'
const yoga = createYoga({ schema, logging: false, endpoint })
const yoga = createYoga({ schema, logging: false, graphqlEndpoint: endpoint })

it('should reject other paths if specific endpoint path is provided', async () => {
const response = await request(yoga).get('/graphql')
Expand Down Expand Up @@ -1048,7 +1048,7 @@ describe('Browser', () => {
schema: createTestSchema(),
cors: () => cors,
logging: false,
endpoint,
graphqlEndpoint: endpoint,
plugins: [
useLiveQuery({
liveQueryStore,
Expand Down Expand Up @@ -1224,7 +1224,7 @@ describe('Browser', () => {
})

test('should show BigInt correctly', async () => {
await page.goto(`http://localhost:4000/${endpoint}`)
await page.goto(`http://localhost:4000${endpoint}`)
await typeOperationText(`{ bigint }`)
await page.click('.execute-button')
const resultContents = await waitForResult()
Expand Down Expand Up @@ -1396,3 +1396,34 @@ describe('Browser', () => {
})
})
})

it('should return 404 if request path does not match with the defined endpoint', async () => {
const hostname = '127.0.0.1'
const port = 4000 + Math.floor(Math.random() * 1000)
const endpoint = '/mypath'
const yoga = createYoga({
graphqlEndpoint: endpoint,
logging: false,
})
const server = createServer(yoga)
const url = `http://${hostname}:${port}${endpoint}`
try {
await new Promise<void>((resolve) =>
server.listen(port, hostname, () => resolve()),
)
const response = await fetch(
url + '?query=' + encodeURIComponent('{ __typename }'),
)
expect(response.status).toEqual(200)
const response2 = await fetch(
url.replace('mypath', 'yourpath') +
'?query=' +
encodeURIComponent('{ __typename }'),
)
expect(response2.status).toEqual(404)
} finally {
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
)
}
})
60 changes: 60 additions & 0 deletions packages/graphql-yoga/src/plugins/useCheckEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Plugin } from './types'

export function useCheckEndpoint(graphqlEndpoint: string): Plugin {
return {
onRequest({ request, fetchAPI, endResponse }) {
// new URL is slow
const { pathname: requestPath } = new URL(request.url)
if (requestPath !== graphqlEndpoint) {
const errorMessage = `
<html>
<head>
<title>GraphQL Yoga - 404 Not Found</title>
<style>
body {
font-family: monospace;
font-size: 12px;
margin: 10px auto;
text-align: center;
}
code {
background: #d5d5d5;
color: brown;
}
textarea {
width: 450px;
}
</style>
</head>
<body>
<p>Unable to <code>${request.method}</code> <code>${requestPath}</code>
<hr>
<p>GraphQL Endpoint is set to <code>${graphqlEndpoint}</code> now.<p>
<p>
So if you expect it to be <code>${requestPath}</code>
please add <code>graphqlEndpoint: '${requestPath}'</code> to GraphQL Yoga configuration like below;
</p>
<textarea readonly rows="10">
import { createYoga } from 'graphql-yoga';
import { schema } from './schema.js';
const yoga = createYoga({
schema,
graphqlEndpoint: '${requestPath}',
})
</textarea>
</body>
</html>
`
endResponse(
new fetchAPI.Response(errorMessage, {
status: 404,
statusText: 'Not Found',
headers: {
'Content-Type': 'text/html',
},
}),
)
}
},
}
}
35 changes: 14 additions & 21 deletions packages/graphql-yoga/src/plugins/useGraphiQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ export type GraphiQLOptions = {
* Whether to open the variable editor by default. Defaults to `true`.
*/
defaultVariableEditorOpen?: boolean
/**
* The endpoint requests should be sent. Defaults to `"/graphql"`.
*/
endpoint?: string
/**
* The initial headers to render inside the header editor. Defaults to `"{}"`.
*/
Expand All @@ -48,7 +44,14 @@ export type GraphiQLOptions = {
additionalHeaders?: Record<string, string>
}

export const renderGraphiQL = (opts?: GraphiQLOptions) =>
export type GraphiQLRendererOptions = {
/**
* The endpoint requests should be sent. Defaults to `"/graphql"`.
*/
endpoint: string
} & GraphiQLOptions

export const renderGraphiQL = (opts: GraphiQLRendererOptions) =>
graphiqlHTML
.replace('__TITLE__', opts?.title || 'Yoga GraphiQL')
.replace('__OPTS__', JSON.stringify(opts ?? {}))
Expand All @@ -66,16 +69,16 @@ export type GraphiQLOptionsOrFactory<TServerContext> =
| boolean

export interface GraphiQLPluginConfig<TServerContext> {
endpoint?: string
graphqlEndpoint: string
options?: GraphiQLOptionsOrFactory<TServerContext>
render?(options?: GraphiQLOptions): PromiseOrValue<BodyInit>
render?(options: GraphiQLRendererOptions): PromiseOrValue<BodyInit>
logger?: YogaLogger
}

export function useGraphiQL<TServerContext>(
config?: GraphiQLPluginConfig<TServerContext>,
config: GraphiQLPluginConfig<TServerContext>,
): Plugin<{}, TServerContext> {
const logger = config?.logger ?? console
const logger = config.logger ?? console
let graphiqlOptionsFactory: GraphiQLOptionsFactory<TServerContext>
if (typeof config?.options === 'function') {
graphiqlOptionsFactory = config?.options
Expand All @@ -92,17 +95,7 @@ export function useGraphiQL<TServerContext>(
return {
async onRequest({ request, serverContext, fetchAPI, endResponse }) {
const requestPath = request.url.split('?')[0]
if (config?.endpoint != null && !requestPath.endsWith(config?.endpoint)) {
logger.debug(`Responding 404 Not Found`)
const response = new fetchAPI.Response(
`Unable to ${request.method} ${requestPath}`,
{
status: 404,
statusText: `Not Found`,
},
)
endResponse(response)
} else if (shouldRenderGraphiQL(request)) {
if (shouldRenderGraphiQL(request)) {
logger.debug(`Rendering GraphiQL`)
const graphiqlOptions = graphiqlOptionsFactory(
request,
Expand All @@ -111,7 +104,7 @@ export function useGraphiQL<TServerContext>(

if (graphiqlOptions) {
const graphiQLBody = await renderer({
endpoint: config?.endpoint,
endpoint: config.graphqlEndpoint,
...(graphiqlOptions === true ? {} : graphiqlOptions),
})

Expand Down
19 changes: 12 additions & 7 deletions packages/graphql-yoga/src/plugins/useHealthCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import { Plugin } from './types.js'
export interface HealthCheckPluginOptions {
id?: string
logger?: YogaLogger
healthCheckEndpoint?: string
readinessCheckEndpoint?: string
}

export function useHealthCheck(options?: HealthCheckPluginOptions): Plugin {
const id = options?.id || Date.now().toString()
const logger = options?.logger || console
export function useHealthCheck({
id = Date.now().toString(),
logger = console,
healthCheckEndpoint = '/health',
readinessCheckEndpoint = '/readiness',
}: HealthCheckPluginOptions = {}): Plugin {
return {
async onRequest({ request, endResponse, fetchAPI }) {
const requestPath = request.url.split('?')[0]
if (requestPath.endsWith('/health')) {
const { pathname: requestPath } = new URL(request.url)
if (requestPath === healthCheckEndpoint) {
logger.debug(`Responding Health Check`)
const response = new fetchAPI.Response(
JSON.stringify({
Expand All @@ -28,10 +33,10 @@ export function useHealthCheck(options?: HealthCheckPluginOptions): Plugin {
},
)
endResponse(response)
} else if (requestPath.endsWith('/readiness')) {
} else if (requestPath === readinessCheckEndpoint) {
logger.debug(`Responding Readiness Check`)
const readinessResponse = await fetchAPI.fetch(
request.url.replace('/readiness', '/health'),
request.url.replace(readinessCheckEndpoint, healthCheckEndpoint),
)
const { message } = await readinessResponse.json()
if (
Expand Down
Loading

0 comments on commit 94db79d

Please sign in to comment.