Skip to content

Commit b4e9a0a

Browse files
Merge pull request #3544 from nestjs/feat/graphiql-apollo-support
feat(apollo): add graphiql playground support
2 parents 8dd8847 + 08f18b9 commit b4e9a0a

File tree

7 files changed

+345
-1
lines changed

7 files changed

+345
-1
lines changed

packages/apollo/lib/drivers/apollo-base.driver.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { isFunction } from '@nestjs/common/utils/shared.utils';
1313
import { AbstractGraphQLDriver } from '@nestjs/graphql';
1414
import { GraphQLError, GraphQLFormattedError } from 'graphql';
1515
import * as omit from 'lodash.omit';
16+
import { GraphiQLPlaygroundPlugin } from '../graphiql/graphiql-playground.plugin';
17+
import { GraphiQLOptions } from '../graphiql/interfaces/graphiql-options.interface';
1618
import { ApolloDriverConfig } from '../interfaces';
1719
import { createAsyncIterator } from '../utils/async-iterator.util';
1820

@@ -56,7 +58,16 @@ export abstract class ApolloBaseDriver<
5658
stopOnTerminationSignals: false,
5759
};
5860

59-
if (
61+
if (options.graphiql) {
62+
const graphiQlPlaygroundOpts: GraphiQLOptions =
63+
typeof options.graphiql === 'object' ? options.graphiql : {};
64+
graphiQlPlaygroundOpts.url ??= options.path;
65+
66+
defaults = {
67+
...defaults,
68+
plugins: [new GraphiQLPlaygroundPlugin(graphiQlPlaygroundOpts)],
69+
};
70+
} else if (
6071
(options.playground === undefined &&
6172
process.env.NODE_ENV !== 'production') ||
6273
options.playground
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { GraphiQLOptions } from './interfaces/graphiql-options.interface';
2+
3+
export class GraphiQLHTMLFactory {
4+
create(options: GraphiQLOptions): string {
5+
const shouldPersistHeaders = options.shouldPersistHeaders ?? true;
6+
const isHeadersEditorEnabled = options.isHeadersEditorEnabled ?? true;
7+
const headers = options.headers ?? {};
8+
const url = options.url ?? '/graphql';
9+
const html = `
10+
<!--
11+
* Copyright (c) 2021 GraphQL Contributors
12+
* All rights reserved.
13+
*
14+
* This source code is licensed under the license found in the
15+
* LICENSE file in the root directory of this source tree.
16+
-->
17+
<!DOCTYPE html>
18+
<html lang="en">
19+
<head>
20+
<title>GraphiQL</title>
21+
<style>
22+
body {
23+
height: 100%;
24+
margin: 0;
25+
width: 100%;
26+
overflow: hidden;
27+
}
28+
29+
#graphiql {
30+
height: 100vh;
31+
}
32+
</style>
33+
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
34+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
35+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/graphiql.min.css" />
36+
</head>
37+
38+
<body>
39+
<div id="graphiql">Loading...</div>
40+
<script
41+
src="https://unpkg.com/[email protected]/graphiql.min.js"
42+
type="application/javascript"
43+
></script>
44+
<script>
45+
const fetcher = GraphiQL.createFetcher({
46+
url: '${url}',
47+
subscriptionUrl: '${url}',
48+
headers: ${JSON.stringify(headers)},
49+
})
50+
ReactDOM.render(
51+
React.createElement(GraphiQL, {
52+
fetcher,
53+
defaultEditorToolsVisibility: true,
54+
shouldPersistHeaders: ${shouldPersistHeaders ? 'true' : 'false'},
55+
isHeadersEditorEnabled: ${isHeadersEditorEnabled ? 'true' : 'false'},
56+
}),
57+
document.getElementById('graphiql'),
58+
);
59+
</script>
60+
</body>
61+
</html>
62+
`;
63+
return html;
64+
}
65+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ApolloServerPlugin } from '@apollo/server';
2+
import { GraphiQLHTMLFactory } from './graphiql-html.factory';
3+
import { GraphiQLOptions } from './interfaces/graphiql-options.interface';
4+
5+
export class GraphiQLPlaygroundPlugin implements ApolloServerPlugin {
6+
private readonly graphiqlHTMLFactory = new GraphiQLHTMLFactory();
7+
8+
constructor(private readonly options: GraphiQLOptions) {}
9+
10+
async serverWillStart() {
11+
const html = this.graphiqlHTMLFactory.create(this.options);
12+
return {
13+
async renderLandingPage() {
14+
return { html };
15+
},
16+
};
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @ref https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/GraphiQL.tsx#L97
3+
* @ref https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/GraphiQL.tsx#L192
4+
*/
5+
export interface GraphiQLOptions {
6+
url?: string;
7+
/**
8+
* Headers you can provide statically.
9+
*
10+
* If you enable the headers editor and the user provides
11+
* A header you set statically here, it will be overridden by their value.
12+
*/
13+
headers?: Record<string, string>;
14+
/**
15+
* This prop toggles if the contents of the headers editor are persisted in
16+
* storage.
17+
* @default true
18+
*/
19+
shouldPersistHeaders?: boolean;
20+
/**
21+
* Toggle if the headers editor should be shown inside the editor tools.
22+
* @default true
23+
*/
24+
isHeadersEditorEnabled?: boolean;
25+
}

packages/apollo/lib/interfaces/apollo-driver-config.interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
GqlOptionsFactory,
77
SubscriptionConfig,
88
} from '@nestjs/graphql';
9+
import { GraphiQLOptions } from '../graphiql/interfaces/graphiql-options.interface';
910

1011
/**
1112
* @publicApi
@@ -39,9 +40,15 @@ export interface ApolloDriverConfig
3940

4041
/**
4142
* GraphQL playground options.
43+
* The built-in playground is deprecated and will be replaced with GraphiQL in the future.
4244
*/
4345
playground?: boolean | ApolloServerPluginLandingPageGraphQLPlaygroundOptions;
4446

47+
/**
48+
* GraphiQL options, or a boolean to enable GraphiQL with default options.
49+
*/
50+
graphiql?: boolean | GraphiQLOptions;
51+
4552
/**
4653
* If enabled, will register a global interceptor that automatically maps
4754
* "HttpException" class instances to corresponding Apollo errors.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import * as request from 'supertest';
4+
import { GraphiQLPlaygroundModule } from '../graphql/graphiql-playground.module';
5+
6+
describe('GraphiQL Playground', () => {
7+
let app: INestApplication;
8+
9+
describe('when "graphiql" is true', () => {
10+
beforeEach(async () => {
11+
const module = await Test.createTestingModule({
12+
imports: [GraphiQLPlaygroundModule.withEnabled()],
13+
}).compile();
14+
15+
app = module.createNestApplication();
16+
await app.init();
17+
});
18+
19+
it(`should render GraphiQL Playground`, (done) => {
20+
request(app.getHttpServer())
21+
.get('/graphql')
22+
.set('Accept', 'text/html')
23+
.expect(200, (err, res) => {
24+
if (err) {
25+
throw err;
26+
}
27+
expect(res.text).toEqual(`
28+
<!--
29+
* Copyright (c) 2021 GraphQL Contributors
30+
* All rights reserved.
31+
*
32+
* This source code is licensed under the license found in the
33+
* LICENSE file in the root directory of this source tree.
34+
-->
35+
<!DOCTYPE html>
36+
<html lang="en">
37+
<head>
38+
<title>GraphiQL</title>
39+
<style>
40+
body {
41+
height: 100%;
42+
margin: 0;
43+
width: 100%;
44+
overflow: hidden;
45+
}
46+
47+
#graphiql {
48+
height: 100vh;
49+
}
50+
</style>
51+
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
52+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
53+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/graphiql.min.css" />
54+
</head>
55+
56+
<body>
57+
<div id="graphiql">Loading...</div>
58+
<script
59+
src="https://unpkg.com/[email protected]/graphiql.min.js"
60+
type="application/javascript"
61+
></script>
62+
<script>
63+
const fetcher = GraphiQL.createFetcher({
64+
url: '/graphql',
65+
subscriptionUrl: '/graphql',
66+
headers: {},
67+
})
68+
ReactDOM.render(
69+
React.createElement(GraphiQL, {
70+
fetcher,
71+
defaultEditorToolsVisibility: true,
72+
shouldPersistHeaders: true,
73+
isHeadersEditorEnabled: true,
74+
}),
75+
document.getElementById('graphiql'),
76+
);
77+
</script>
78+
</body>
79+
</html>
80+
`);
81+
done();
82+
});
83+
});
84+
85+
afterEach(async () => {
86+
await app.close();
87+
});
88+
});
89+
90+
describe('when "graphiql" is an options object', () => {
91+
beforeEach(async () => {
92+
const module = await Test.createTestingModule({
93+
imports: [
94+
GraphiQLPlaygroundModule.withEnabledAndCustomized({
95+
headers: {
96+
'x-custom-header': 'custom-value',
97+
},
98+
shouldPersistHeaders: false,
99+
isHeadersEditorEnabled: false,
100+
}),
101+
],
102+
}).compile();
103+
104+
app = module.createNestApplication();
105+
await app.init();
106+
});
107+
108+
it(`should render GraphiQL Playground`, (done) => {
109+
request(app.getHttpServer())
110+
.get('/graphql')
111+
.set('Accept', 'text/html')
112+
.expect(200, (err, res) => {
113+
if (err) {
114+
throw err;
115+
}
116+
expect(res.text).toEqual(`
117+
<!--
118+
* Copyright (c) 2021 GraphQL Contributors
119+
* All rights reserved.
120+
*
121+
* This source code is licensed under the license found in the
122+
* LICENSE file in the root directory of this source tree.
123+
-->
124+
<!DOCTYPE html>
125+
<html lang="en">
126+
<head>
127+
<title>GraphiQL</title>
128+
<style>
129+
body {
130+
height: 100%;
131+
margin: 0;
132+
width: 100%;
133+
overflow: hidden;
134+
}
135+
136+
#graphiql {
137+
height: 100vh;
138+
}
139+
</style>
140+
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
141+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
142+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/graphiql.min.css" />
143+
</head>
144+
145+
<body>
146+
<div id="graphiql">Loading...</div>
147+
<script
148+
src="https://unpkg.com/[email protected]/graphiql.min.js"
149+
type="application/javascript"
150+
></script>
151+
<script>
152+
const fetcher = GraphiQL.createFetcher({
153+
url: '/graphql',
154+
subscriptionUrl: '/graphql',
155+
headers: {"x-custom-header":"custom-value"},
156+
})
157+
ReactDOM.render(
158+
React.createElement(GraphiQL, {
159+
fetcher,
160+
defaultEditorToolsVisibility: true,
161+
shouldPersistHeaders: false,
162+
isHeadersEditorEnabled: false,
163+
}),
164+
document.getElementById('graphiql'),
165+
);
166+
</script>
167+
</body>
168+
</html>
169+
`);
170+
done();
171+
});
172+
});
173+
174+
afterEach(async () => {
175+
await app.close();
176+
});
177+
});
178+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Module } from '@nestjs/common';
2+
import { GraphQLModule } from '@nestjs/graphql';
3+
import { join } from 'path';
4+
import { ApolloDriverConfig } from '../../lib';
5+
import { ApolloDriver } from '../../lib/drivers';
6+
import { GraphiQLOptions } from '../../lib/graphiql/interfaces/graphiql-options.interface';
7+
import { CatsModule } from './cats/cats.module';
8+
9+
@Module({
10+
imports: [CatsModule],
11+
})
12+
export class GraphiQLPlaygroundModule {
13+
static withEnabled() {
14+
return {
15+
module: GraphiQLPlaygroundModule,
16+
imports: [
17+
GraphQLModule.forRoot<ApolloDriverConfig>({
18+
driver: ApolloDriver,
19+
csrfPrevention: false,
20+
graphiql: true,
21+
typePaths: [join(__dirname, '**', '*.graphql')],
22+
}),
23+
],
24+
};
25+
}
26+
27+
static withEnabledAndCustomized(options: GraphiQLOptions) {
28+
return {
29+
module: GraphiQLPlaygroundModule,
30+
imports: [
31+
GraphQLModule.forRoot<ApolloDriverConfig>({
32+
driver: ApolloDriver,
33+
csrfPrevention: false,
34+
typePaths: [join(__dirname, '**', '*.graphql')],
35+
graphiql: options,
36+
}),
37+
],
38+
};
39+
}
40+
}

0 commit comments

Comments
 (0)