Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor endpoint handling #1455

Merged
merged 12 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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