Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit cb60d22

Browse files
authored
Merge pull request #104 from graphql-java/graphiql-improvements
GraphiQL improvements
2 parents b6049b2 + abd9eb4 commit cb60d22

9 files changed

Lines changed: 242 additions & 33 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,26 @@ graphiql:
112112
cdn:
113113
enabled: false
114114
version: 0.11.11
115+
props:
116+
resources:
117+
query: query.graphql
118+
defaultQuery: defaultQuery.graphql
119+
variables: variables.graphql
120+
variables:
121+
editorTheme: "solarized light"
122+
headers:
123+
Authorization: "Bearer <your-token>"
115124
```
116125
By default GraphiQL is served from within the package. This can be configured to be served from CDN instead,
117126
by setting the property `graphiql.cdn.enabled` to `true`.
118127

128+
You are able to set the GraphiQL props as well. The `graphiql.props.variables` group can contain any of the props
129+
as defined at [GraphiQL Usage](https://github.com/graphql/graphiql#usage). Since setting (large) queries in the
130+
properties like this isn't very readable, you can use the properties in the `graphiql.props.resources` group
131+
to set the classpath resources that should be loaded.
132+
133+
Headers that are used when sending the GraphiQL queries can be set by defining them in the `graphiql.headers` group.
134+
119135
# Supported GraphQL-Java Libraries
120136

121137
The following libraries have auto-configuration classes for creating a `GraphQLSchema`.

example-graphql-tools/src/main/resources/application.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@ spring:
33
name: graphql-java-tools-app
44
server:
55
port: 9000
6+
graphiql:
7+
props:
8+
resources:
9+
query: defaultQuery.graphql
10+
defaultQuery: defaultQuery.graphql
11+
variables: defaultQuery.graphql
12+
headers:
13+
Authorization: "Bearer asdfasdf"
14+
Content-Type: "application/json;charset=UTF-8"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
query {
2+
echo(string: "echo")
3+
}
Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package com.oembedler.moon.graphiql.boot;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
35
import org.apache.commons.lang3.StringUtils;
46
import org.apache.commons.lang3.text.StrSubstitutor;
7+
import org.springframework.beans.factory.annotation.Autowired;
58
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.core.env.Environment;
610
import org.springframework.core.io.ClassPathResource;
11+
import org.springframework.http.MediaType;
712
import org.springframework.stereotype.Controller;
813
import org.springframework.util.StreamUtils;
914
import org.springframework.web.bind.annotation.PathVariable;
@@ -18,13 +23,15 @@
1823
import java.nio.charset.Charset;
1924
import java.util.HashMap;
2025
import java.util.Map;
26+
import java.util.Properties;
2127

2228
/**
2329
* @author Andrew Potter
2430
*/
2531
@Controller
2632
public class GraphiQLController {
2733

34+
private static final String CDNJS_CLOUDFLARE_COM_AJAX_LIBS_GRAPHIQL = "//cdnjs.cloudflare.com/ajax/libs/graphiql/";
2835
@Value("${graphiql.endpoint:/graphql}")
2936
private String graphqlEndpoint;
3037

@@ -37,56 +44,93 @@ public class GraphiQLController {
3744
@Value("${graphiql.cdn.version:0.11.11}")
3845
private String graphiqlCdnVersion;
3946

47+
@Autowired
48+
private Environment environment;
49+
4050
private String template;
51+
private String props;
52+
private String headers;
4153

4254
@PostConstruct
43-
public void loadTemplate() throws IOException {
55+
public void onceConstructed() throws IOException {
56+
loadTemplate();
57+
loadProps();
58+
loadHeaders();
59+
}
60+
61+
private void loadTemplate() throws IOException {
4462
try (InputStream inputStream = new ClassPathResource("graphiql.html").getInputStream()) {
4563
template = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
4664
}
4765
}
4866

67+
private void loadProps() throws IOException {
68+
props = new PropsLoader(environment).load();
69+
}
70+
71+
private void loadHeaders() throws JsonProcessingException {
72+
PropertyGroupReader propertyReader = new PropertyGroupReader(environment, "graphiql.headers.");
73+
Properties headerProperties = propertyReader.load();
74+
addIfAbsent(headerProperties, "Accept");
75+
addIfAbsent(headerProperties, "Content-Type");
76+
this.headers = new ObjectMapper().writeValueAsString(headerProperties);
77+
}
78+
79+
private void addIfAbsent(Properties headerProperties, String header) {
80+
if (!headerProperties.containsKey(header)) {
81+
headerProperties.setProperty(header, MediaType.APPLICATION_JSON_VALUE);
82+
}
83+
}
84+
4985
@RequestMapping(value = "${graphiql.mapping:/graphiql}")
5086
public void graphiql(HttpServletRequest request, HttpServletResponse response, @PathVariable Map<String, String> params) throws IOException {
5187
response.setContentType("text/html; charset=UTF-8");
5288

53-
Map<String, String> replacements = new HashMap<>();
54-
55-
String graphiqlCssUrl = "/vendor/graphiql.min.css";
56-
String graphiqlJsUrl = "/vendor/graphiql.min.js";
89+
String endpoint = constructGraphQlEndpoint(request, params);
90+
Map<String, String> replacements = getReplacements(endpoint);
5791

58-
if (graphiqlCdnEnabled && StringUtils.isNotBlank(graphiqlCdnVersion)) {
59-
graphiqlCssUrl = "//cdnjs.cloudflare.com/ajax/libs/graphiql/" + graphiqlCdnVersion + "/graphiql.min.css";
60-
graphiqlJsUrl = "//cdnjs.cloudflare.com/ajax/libs/graphiql/" + graphiqlCdnVersion + "/graphiql.min.js";
61-
}
62-
63-
String endpoint = constructGraphQlEndpoint(params);
64-
if (StringUtils.isNotBlank(request.getContextPath()) && !endpoint.startsWith(request.getContextPath())) {
65-
endpoint = request.getContextPath() + endpoint;
66-
}
92+
String populatedTemplate = StrSubstitutor.replace(template, replacements);
93+
populatedTemplate = addContextPathIfEnabled(request, populatedTemplate);
94+
response.getOutputStream().write(populatedTemplate.getBytes(Charset.defaultCharset()));
95+
}
6796

97+
private Map<String, String> getReplacements(String endpoint) {
98+
Map<String, String> replacements = new HashMap<>();
6899
replacements.put("graphqlEndpoint", endpoint);
69100
replacements.put("pageTitle", pageTitle);
70-
replacements.put("graphiqlCssUrl", graphiqlCssUrl);
71-
replacements.put("graphiqlJsUrl", graphiqlJsUrl);
101+
replacements.put("graphiqlCssUrl", graphiqlUrl("graphiql.min.css"));
102+
replacements.put("graphiqlJsUrl", graphiqlUrl("graphiql.min.js"));
103+
replacements.put("props", props);
104+
replacements.put("headers", headers);
105+
return replacements;
106+
}
72107

73-
String populatedTemplate = StrSubstitutor.replace(template, replacements);
108+
private String graphiqlUrl(String filename) {
109+
if (graphiqlCdnEnabled && StringUtils.isNotBlank(graphiqlCdnVersion)) {
110+
return CDNJS_CLOUDFLARE_COM_AJAX_LIBS_GRAPHIQL + graphiqlCdnVersion + "/" + filename;
111+
}
112+
return "/vendor/" + filename;
113+
}
74114

115+
private String addContextPathIfEnabled(HttpServletRequest request, String populatedTemplate) {
75116
if (StringUtils.isNotBlank(request.getContextPath())) {
76117
String vendorPathWithContext = String.format("%s/vendor", request.getContextPath());
77118
populatedTemplate = populatedTemplate
78119
.replaceAll("src=\"/vendor", "src=\"" + vendorPathWithContext)
79120
.replaceAll("href=\"/vendor", "href=\"" + vendorPathWithContext);
80121
}
81-
82-
response.getOutputStream().write(populatedTemplate.getBytes(Charset.defaultCharset()));
122+
return populatedTemplate;
83123
}
84124

85-
private String constructGraphQlEndpoint(@RequestParam Map<String, String> params) {
125+
private String constructGraphQlEndpoint(HttpServletRequest request, @RequestParam Map<String, String> params) {
86126
String endpoint = graphqlEndpoint;
87127
for (Map.Entry<String, String> param : params.entrySet()) {
88128
endpoint = endpoint.replaceAll("\\{" + param.getKey() + "}", param.getValue());
89129
}
130+
if (StringUtils.isNotBlank(request.getContextPath()) && !endpoint.startsWith(request.getContextPath())) {
131+
return request.getContextPath() + endpoint;
132+
}
90133
return endpoint;
91134
}
135+
92136
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.oembedler.moon.graphiql.boot;
2+
3+
import org.springframework.core.env.ConfigurableEnvironment;
4+
import org.springframework.core.env.EnumerablePropertySource;
5+
import org.springframework.core.env.Environment;
6+
import org.springframework.core.env.PropertySource;
7+
8+
import java.util.*;
9+
import java.util.stream.Stream;
10+
import java.util.stream.StreamSupport;
11+
12+
class PropertyGroupReader {
13+
14+
private Environment environment;
15+
private String prefix;
16+
private Properties props;
17+
18+
PropertyGroupReader(Environment environment, String prefix) {
19+
this.environment = Objects.requireNonNull(environment);
20+
this.prefix = Optional.ofNullable(prefix).orElse("");
21+
}
22+
23+
Properties load() {
24+
if (props == null) {
25+
props = new Properties();
26+
loadProps();
27+
}
28+
return props;
29+
}
30+
31+
private void loadProps() {
32+
streamOfPropertySources().forEach(propertySource ->
33+
Arrays.stream(propertySource.getPropertyNames())
34+
.filter(this::isWanted)
35+
.forEach(key -> add(propertySource, key)));
36+
}
37+
38+
private Stream<EnumerablePropertySource> streamOfPropertySources() {
39+
if (environment instanceof ConfigurableEnvironment) {
40+
Iterator<PropertySource<?>> iterator = ((ConfigurableEnvironment) environment).getPropertySources().iterator();
41+
Iterable<PropertySource<?>> iterable = () -> iterator;
42+
return StreamSupport.stream(iterable.spliterator(), false)
43+
.filter(EnumerablePropertySource.class::isInstance)
44+
.map(EnumerablePropertySource.class::cast);
45+
}
46+
return Stream.empty();
47+
}
48+
49+
private String withoutPrefix(String key) {
50+
return key.replace(prefix, "");
51+
}
52+
53+
private boolean isWanted(String key) {
54+
return key.startsWith(prefix);
55+
}
56+
57+
private Object add(EnumerablePropertySource propertySource, String key) {
58+
return props.put(withoutPrefix(key), propertySource.getProperty(key));
59+
}
60+
61+
}
62+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.oembedler.moon.graphiql.boot;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import org.springframework.core.env.Environment;
5+
import org.springframework.core.io.ClassPathResource;
6+
import org.springframework.core.io.Resource;
7+
import org.springframework.util.StreamUtils;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.Optional;
13+
import java.util.Properties;
14+
15+
class PropsLoader {
16+
17+
private static final String GRAPHIQL_PROPS_PREFIX = "graphiql.props.";
18+
private static final String GRAPHIQL_PROPS_RESOURCES_PREFIX = GRAPHIQL_PROPS_PREFIX + "resources.";
19+
private static final String GRAPHIQL_PROPS_VALUES_PREFIX = GRAPHIQL_PROPS_PREFIX + "values.";
20+
21+
private Environment environment;
22+
23+
PropsLoader(Environment environment) {
24+
this.environment = environment;
25+
}
26+
27+
String load() throws IOException {
28+
PropertyGroupReader reader = new PropertyGroupReader(environment, GRAPHIQL_PROPS_VALUES_PREFIX);
29+
Properties props = reader.load();
30+
31+
ObjectMapper objectMapper = new ObjectMapper();
32+
loadPropFromResource("defaultQuery").ifPresent(it -> props.put("defaultQuery", it));
33+
loadPropFromResource("query").ifPresent(it -> props.put("query", it));
34+
loadPropFromResource("variables").ifPresent(it -> props.put("variables", it));
35+
return objectMapper.writeValueAsString(props);
36+
}
37+
38+
private Optional<String> loadPropFromResource(String prop) throws IOException {
39+
String property = GRAPHIQL_PROPS_RESOURCES_PREFIX + prop;
40+
if (environment.containsProperty(property)) {
41+
String location = environment.getProperty(property);
42+
Resource resource = new ClassPathResource(location);
43+
return Optional.of(loadResource(resource));
44+
}
45+
return Optional.empty();
46+
}
47+
48+
private String loadResource(Resource resource) throws IOException {
49+
try (InputStream inputStream = resource.getInputStream()) {
50+
return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
51+
}
52+
}
53+
54+
}

graphiql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@
2929
"name": "graphiql.cdn.version",
3030
"defaultValue": "0.11.11",
3131
"type": "java.lang.String"
32+
},
33+
{
34+
"name": "graphiql.props.resources.query",
35+
"defaultValue": null,
36+
"type": "java.lang.String"
37+
},
38+
{
39+
"name": "graphiql.props.resources.defaultQuery",
40+
"defaultValue": null,
41+
"type": "java.lang.String"
42+
},
43+
{
44+
"name": "graphiql.props.resources.variables",
45+
"defaultValue": null,
46+
"type": "java.lang.String"
3247
}
3348
]
3449
}

graphiql-spring-boot-autoconfigure/src/main/resources/graphiql.html

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
history.replaceState(null, null, newSearch);
8989
}
9090

91+
var headers = ${headers};
92+
9193
// Defines a GraphQL fetcher using the fetch API. You're not required to
9294
// use fetch, and could instead implement graphQLFetcher however you like,
9395
// as long as it returns a Promise or Observable.
@@ -96,10 +98,7 @@
9698
// Change this to point wherever you host your GraphQL server.
9799
return fetch('${graphqlEndpoint}', {
98100
method: 'post',
99-
headers: {
100-
'Accept': 'application/json',
101-
'Content-Type': 'application/json'
102-
},
101+
headers: headers,
103102
body: JSON.stringify(graphQLParams),
104103
credentials: 'include'
105104
}).then(function (response) {
@@ -127,17 +126,24 @@
127126
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(newUri, { reconnect: true });
128127
var subscriptionsFetcher = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher);
129128

129+
var props = ${props};
130+
if (parameters.query) {
131+
props.query = parameters.query;
132+
}
133+
if (parameters.variables) {
134+
props.variables = parameters.variables;
135+
}
136+
if (parameters.operationName) {
137+
props.operationName = parameters.operationName;
138+
}
139+
props.fetcher = subscriptionsFetcher;
140+
props.onEditQuery = onEditQuery;
141+
props.onEditVariables = onEditVariables;
142+
props.onEditOperationName = onEditOperationName;
143+
130144
// Render <GraphiQL /> into the body.
131145
ReactDOM.render(
132-
React.createElement(GraphiQL, {
133-
fetcher: subscriptionsFetcher,
134-
query: parameters.query,
135-
variables: parameters.variables,
136-
operationName: parameters.operationName,
137-
onEditQuery: onEditQuery,
138-
onEditVariables: onEditVariables,
139-
onEditOperationName: onEditOperationName
140-
}),
146+
React.createElement(GraphiQL, props),
141147
document.body
142148
);
143149
</script>

graphql-spring-boot-starter-test/README.md

Whitespace-only changes.

0 commit comments

Comments
 (0)