Skip to content

Commit

Permalink
feat(playground): enable GraphQL playground using GraphiQL (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
metrue authored Mar 18, 2024
1 parent 2bcc7cd commit 5fb59fb
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 7 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,6 @@ app.use(responseTime);
The builtin middlewares are,

* [JWT](src/middleware/jwt)
* [GraphiQL](src/middleware/graphiql)
* [CORS](src/middleware/cors)
* [wallclock](src/middleware/wallclock)
2 changes: 1 addition & 1 deletion examples/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"wrangler": "^3.0.0"
},
"dependencies": {
"edgeql": "^0.1.9",
"edgeql": "0.2.2",
"graphql": "^16.8.1"
}
}
2 changes: 2 additions & 0 deletions examples/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EdgeQL } from 'edgeql'
import type { Context } from 'edgeql'
import { graphiql } from 'edgeql/graphiql'
import {
GraphQLSchema,
GraphQLObjectType,
Expand Down Expand Up @@ -48,5 +49,6 @@ type Query {

app.handle(helloworld)
app.handle(clock)
app.use(graphiql())

export default app
2 changes: 1 addition & 1 deletion examples/cloudflare/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
/* Modules */
"module": "es2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
Expand Down
7 changes: 3 additions & 4 deletions examples/cloudflare/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,9 @@ dset@^3.1.2:
resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.3.tgz#c194147f159841148e8e34ca41f638556d9542d2"
integrity sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==

edgeql@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/edgeql/-/edgeql-0.1.9.tgz#a94ed2cbf9555ee0bc2ffdfa86c5edff10ffbf7c"
integrity sha512-OO9RWmar+owXW8YxuAij6XuGLVGQsK/cO5N6/BXduUejErrG9LdqckpXY8DJ8AjZmMBB+m4AQkz9ZgYGR8eREg==
edgeql@../../edgeql-v0.2.2.tgz:
version "0.2.2"
resolved "../../edgeql-v0.2.2.tgz#c66ad311534ffffd3068187dd08cfe504bf8ef76"
dependencies:
"@graphql-tools/schema" "^10.0.2"
"@hono/node-server" "^1.8.2"
Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "edgeql",
"version": "0.2.1",
"version": "0.2.2",
"author": "Minghe Huang <h.minghe@gmail.com> (https://github.com/metrue)",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -79,6 +79,11 @@
"import": "./dist/esm/middleware/cors/index.js",
"require": "./dist/cjs/middleware/cors/index.js"
},
"./graphiql": {
"types": "./dist/esm/middleware/graphiql/index.d.ts",
"import": "./dist/esm/middleware/graphiql/index.js",
"require": "./dist/cjs/middleware/graphiql/index.js"
},
"./utils/jwt": {
"types": "./dist/esm/utils/jwt/index.d.ts",
"import": "./dist/esm/utils/jwt/index.js",
Expand All @@ -104,6 +109,9 @@
"middleware/cors": [
"./dist/esm/middleware/cors"
],
"middleware/graphiql": [
"./dist/esm/middleware/graphiql"
],
"utils/jwt": [
"./dist/esm/utils/jwt/index.d.ts"
],
Expand Down
6 changes: 6 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export class EdgeQL {

await compose([...this.middlewares, this.execute])(ctx)

if (ctx.http.headers.get('Content-Type') === 'text/html') {
return ctx.html()
} else if (ctx.http.headers.get('Content-Type') === 'text/plain') {
return ctx.text()
}

return ctx.json()
}

Expand Down
38 changes: 38 additions & 0 deletions src/context/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,42 @@ describe('context', () => {
expect(ctx).not.toBe(null)
expect(ctx.get('k')).toBe('v')
})

it('json', async () => {
const ctx = new Context(
new Request('http://localhost'),
{ Bindings: {}, Variables: {} },
{ waitUntil: () => {}, passThroughOnException: () => {} }
)
ctx.http.body = { data: 'test' }
const res = ctx.json()
expect(res.headers.get('content-type')).toEqual('application/json')
expect(await res.json()).toEqual({
data: 'test',
})
})

it('html', async () => {
const ctx = new Context(
new Request('http://localhost'),
{ Bindings: {}, Variables: {} },
{ waitUntil: () => {}, passThroughOnException: () => {} }
)
ctx.http.body = 'hello world'
const res = ctx.html()
expect(res.headers.get('content-type')).toEqual('text/html')
expect(await res.text()).toEqual('hello world')
})

it('text', async () => {
const ctx = new Context(
new Request('http://localhost'),
{ Bindings: {}, Variables: {} },
{ waitUntil: () => {}, passThroughOnException: () => {} }
)
ctx.http.body = 'hello world'
const res = ctx.text()
expect(res.headers.get('content-type')).toEqual('text/plain')
expect(await res.text()).toEqual('hello world')
})
})
8 changes: 8 additions & 0 deletions src/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export class Context {
return this.http.toJSON()
}

html(): Response {
return this.http.toHtml()
}

text(): Response {
return this.http.toText()
}

set(key: string, value: unknown): void {
this._map ||= {}
this._map[key] = value
Expand Down
11 changes: 11 additions & 0 deletions src/context/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ export class HttpContext {
})
}

toHtml(): Response {
this.headers.set('content-type', 'text/html')

return new Response(this.body, {
status: this.status ?? 200,
headers: this.headers,
})
}

toText(): Response {
this.headers.set('content-type', 'text/plain')

return new Response(this.body, {
status: this.status ?? 200,
headers: this.headers,
Expand Down
23 changes: 23 additions & 0 deletions src/middleware/graphiql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# EdgeQL GraphiQL middleware

```typescript

import type { Context } from 'edgeql'
import { cors } from 'edgeql/cors'
import { graphiql } from 'edgeql/graphiql'
import { NodeEdgeQL } from 'edgeql/node'

const app = new NodeEdgeQL()

app.handle(`
type Query {
hello: String!
}
`, (ctx: Context) => {
return `hello world`
}
)

app.use(graphiql({path: '/playground'}))

```
75 changes: 75 additions & 0 deletions src/middleware/graphiql/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest'
import { EdgeQL } from '../../'
import { graphiql } from '.'

describe('GraphiQL', () => {
it('should load GraphiQL when access the path', async () => {
const app = new EdgeQL()
app.use(graphiql({ path: '/playground' }))
const endpoint = 'http://localhost'
const req = new Request(endpoint + '/playground')
const res = await app.fetch(req)
expect(res.statusText).toEqual('')
expect(res.status).toBe(200)
expect(await res.text()).toEqual(`
<html lang="en">
<head>
<title>GraphiQL</title>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
></script>
<script
src="https://unpkg.com/graphiql/graphiql.min.js"
type="application/javascript"
></script>
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
<script
src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
crossorigin
></script>
<link
rel="stylesheet"
href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css"
/>
</head>
<body>
<div id="graphiql">Loading...</div>
<script>
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
const fetcher = GraphiQL.createFetcher({
url: '${endpoint}',
headers: { 'X-Example-Header': 'foo' },
});
const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
root.render(
React.createElement(GraphiQL, {
fetcher,
defaultEditorToolsVisibility: true,
plugins: [explorerPlugin],
}),
);
</script>
</body>
</html>
`)
})
})
76 changes: 76 additions & 0 deletions src/middleware/graphiql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Context, Middleware, Next } from '../../'

export const graphiql = (options?: { path?: string }): Middleware => {
const p = options?.path ?? '/playground'

return async (ctx: Context, next: Next) => {
const url = new URL(ctx.http.request.url)
if (url.pathname !== p) {
await next()
} else {
const endpoint = `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ''}`
ctx.http.status = 200
ctx.http.headers.set('content-type', 'text/html')
ctx.http.body = `
<html lang="en">
<head>
<title>GraphiQL</title>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
></script>
<script
src="https://unpkg.com/graphiql/graphiql.min.js"
type="application/javascript"
></script>
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
<script
src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
crossorigin
></script>
<link
rel="stylesheet"
href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css"
/>
</head>
<body>
<div id="graphiql">Loading...</div>
<script>
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
const fetcher = GraphiQL.createFetcher({
url: '${endpoint}',
headers: { 'X-Example-Header': 'foo' },
});
const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
root.render(
React.createElement(GraphiQL, {
fetcher,
defaultEditorToolsVisibility: true,
plugins: [explorerPlugin],
}),
);
</script>
</body>
</html>
`
}
}
}

0 comments on commit 5fb59fb

Please sign in to comment.