Skip to content

Commit 1e92316

Browse files
committed
Added tools for introspection (#164)
- Introspection query execute line marker on url in .graphqlconfig - Print schema JSON as SDL line marker
1 parent 771636d commit 1e92316

12 files changed

+724
-20
lines changed

resources/META-INF/plugin.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@
102102
<!-- Editor notifications -->
103103
<editorNotificationProvider implementation="com.intellij.lang.jsgraphql.ide.notifications.GraphQLScopeEditorNotificationProvider"/>
104104

105+
<!-- Introspection -->
106+
<codeInsight.lineMarkerProvider implementationClass="com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionJsonToSDLLineMarkerProvider" language="JSON" />
107+
<codeInsight.lineMarkerProvider implementationClass="com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectEndpointUrlLineMarkerProvider" language="JSON" />
108+
<projectViewNestingRulesProvider implementation="com.intellij.lang.jsgraphql.ide.project.GraphQLIntrospectionProjectViewNestingRulesProvider" />
109+
110+
105111
<!-- v2 above this point -->
106112

107113

src/main/com/intellij/lang/jsgraphql/GraphQLSettings.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,15 @@ public GraphQLScopeResolution getScopeResolution() {
4343
}
4444

4545
public void setScopeResolution(GraphQLScopeResolution scopeResolution) {
46-
this.myState.scopeResolution = scopeResolution;
46+
myState.scopeResolution = scopeResolution;
47+
}
48+
49+
public String getIntrospectionQuery() {
50+
return myState.introspectionQuery;
51+
}
52+
53+
public void setIntrospectionQuery(String introspectionQuery) {
54+
myState.introspectionQuery = introspectionQuery;
4755
}
4856

4957
/**
@@ -53,6 +61,7 @@ public void setScopeResolution(GraphQLScopeResolution scopeResolution) {
5361
*/
5462
static class GraphQLSettingsState {
5563
public GraphQLScopeResolution scopeResolution = GraphQLScopeResolution.ENTIRE_PROJECT;
64+
public String introspectionQuery = "";
5665
}
5766
}
5867

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (c) 2018-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
package com.intellij.lang.jsgraphql.ide.editor;
9+
10+
import com.google.gson.Gson;
11+
import com.intellij.codeHighlighting.Pass;
12+
import com.intellij.codeInsight.daemon.LineMarkerInfo;
13+
import com.intellij.codeInsight.daemon.LineMarkerProvider;
14+
import com.intellij.icons.AllIcons;
15+
import com.intellij.json.psi.JsonObject;
16+
import com.intellij.json.psi.JsonProperty;
17+
import com.intellij.json.psi.JsonStringLiteral;
18+
import com.intellij.json.psi.JsonValue;
19+
import com.intellij.lang.jsgraphql.GraphQLSettings;
20+
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigManager;
21+
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.model.GraphQLConfigEndpoint;
22+
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.model.GraphQLConfigVariableAwareEndpoint;
23+
import com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService;
24+
import com.intellij.notification.Notification;
25+
import com.intellij.notification.NotificationType;
26+
import com.intellij.notification.Notifications;
27+
import com.intellij.openapi.application.ApplicationManager;
28+
import com.intellij.openapi.editor.markup.GutterIconRenderer;
29+
import com.intellij.openapi.progress.ProgressManager;
30+
import com.intellij.openapi.vfs.VirtualFile;
31+
import com.intellij.psi.PsiElement;
32+
import com.intellij.psi.util.PsiTreeUtil;
33+
import graphql.introspection.IntrospectionQuery;
34+
import org.apache.commons.httpclient.HttpClient;
35+
import org.apache.commons.httpclient.methods.PostMethod;
36+
import org.apache.commons.httpclient.methods.StringRequestEntity;
37+
import org.apache.commons.httpclient.params.HttpClientParams;
38+
import org.apache.commons.lang.StringEscapeUtils;
39+
import org.apache.commons.lang.StringUtils;
40+
import org.jetbrains.annotations.NotNull;
41+
import org.jetbrains.annotations.Nullable;
42+
43+
import java.io.IOException;
44+
import java.io.UnsupportedEncodingException;
45+
import java.util.Collection;
46+
import java.util.List;
47+
import java.util.Map;
48+
import java.util.Optional;
49+
import java.util.stream.Stream;
50+
51+
import static com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper.printIntrospectionJsonAsGraphQL;
52+
import static com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService.setHeadersFromOptions;
53+
54+
/**
55+
* Line marker for running an introspection against a configured endpoint url in a .graphqlconfig file
56+
*/
57+
public class GraphQLIntrospectEndpointUrlLineMarkerProvider implements LineMarkerProvider {
58+
@Nullable
59+
@Override
60+
public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) {
61+
if (!GraphQLConfigManager.GRAPHQLCONFIG.equals(element.getContainingFile().getName())) {
62+
return null;
63+
}
64+
if (element instanceof JsonProperty) {
65+
final JsonProperty jsonProperty = (JsonProperty) element;
66+
if ("url".equals(jsonProperty.getName()) && jsonProperty.getValue() instanceof JsonStringLiteral) {
67+
68+
return new LineMarkerInfo<>((JsonStringLiteral) jsonProperty.getValue(), jsonProperty.getValue().getTextRange(), AllIcons.General.Run, Pass.UPDATE_ALL, o -> "Run introspection query to generate GraphQL SDL schema file", (evt, jsonUrl) -> {
69+
70+
final String url = jsonUrl.getValue();
71+
72+
final GraphQLConfigVariableAwareEndpoint endpoint = getEndpoint(url, jsonProperty);
73+
if (endpoint == null) {
74+
return;
75+
}
76+
77+
String schemaPath = getSchemaPath(jsonProperty);
78+
if (schemaPath == null || schemaPath.trim().isEmpty()) {
79+
return;
80+
}
81+
82+
final HttpClient httpClient = new HttpClient(new HttpClientParams());
83+
84+
try {
85+
86+
String query = GraphQLSettings.getSettings(element.getProject()).getIntrospectionQuery();
87+
if (StringUtils.isBlank(query)) {
88+
query = IntrospectionQuery.INTROSPECTION_QUERY;
89+
}
90+
91+
final String requestJson = "{\"query\":\"" + StringEscapeUtils.escapeJavaScript(query) + "\"}";
92+
93+
final PostMethod method = new PostMethod(endpoint.getUrl());
94+
method.setRequestEntity(new StringRequestEntity(requestJson, "application/json", "UTF-8"));
95+
96+
setHeadersFromOptions(endpoint, method);
97+
98+
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
99+
ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true);
100+
try {
101+
httpClient.executeMethod(method);
102+
final String responseJson = Optional.ofNullable(method.getResponseBodyAsString()).orElse("");
103+
ApplicationManager.getApplication().invokeLater(() -> {
104+
try {
105+
JSGraphQLLanguageUIProjectService.getService(jsonProperty.getProject()).showQueryResult(responseJson);
106+
final String schemaAsSDL = printIntrospectionJsonAsGraphQL(responseJson);
107+
VirtualFile virtualFile = element.getContainingFile().getVirtualFile();
108+
GraphQLIntrospectionHelper.createOrUpdateIntrospectionSDLFile(schemaAsSDL, virtualFile, schemaPath, element.getProject());
109+
} catch (Exception e) {
110+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Introspection Error", e.getMessage(), NotificationType.WARNING), element.getProject());
111+
}
112+
});
113+
} catch (IOException e) {
114+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.WARNING), element.getProject());
115+
}
116+
117+
}, "Executing GraphQL Introspection Query", false, jsonProperty.getProject());
118+
119+
120+
} catch (UnsupportedEncodingException | IllegalStateException | IllegalArgumentException e) {
121+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.ERROR), element.getProject());
122+
}
123+
124+
}, GutterIconRenderer.Alignment.CENTER);
125+
}
126+
}
127+
return null;
128+
}
129+
130+
private String getSchemaPath(JsonProperty urlElement) {
131+
JsonObject jsonObject = PsiTreeUtil.getParentOfType(urlElement, JsonObject.class);
132+
String url = urlElement.getValue() instanceof JsonStringLiteral ? ((JsonStringLiteral) urlElement.getValue()).getValue() : "";
133+
while (jsonObject != null) {
134+
JsonProperty schemaPathElement = jsonObject.findProperty("schemaPath");
135+
if (schemaPathElement != null) {
136+
if (schemaPathElement.getValue() instanceof JsonStringLiteral) {
137+
String schemaPath = ((JsonStringLiteral) schemaPathElement.getValue()).getValue();
138+
if (schemaPath.trim().isEmpty()) {
139+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Configuration Error", "The schemaPath must be defined for url " + url, NotificationType.ERROR), urlElement.getProject());
140+
}
141+
return schemaPath;
142+
} else {
143+
break;
144+
}
145+
}
146+
jsonObject = PsiTreeUtil.getParentOfType(jsonObject, JsonObject.class);
147+
}
148+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Configuration Error", "No schemaPath found for url " + url, NotificationType.ERROR), urlElement.getProject());
149+
return null;
150+
}
151+
152+
private GraphQLConfigVariableAwareEndpoint getEndpoint(String url, JsonProperty urlJsonProperty) {
153+
try {
154+
155+
final GraphQLConfigEndpoint endpointConfig = new GraphQLConfigEndpoint("", "", url);
156+
157+
final Stream<JsonProperty> jsonPropertyStream = PsiTreeUtil.getChildrenOfTypeAsList(urlJsonProperty.getParent(), JsonProperty.class).stream();
158+
final Optional<JsonProperty> headers = jsonPropertyStream.filter(p -> "headers".equals(p.getName())).findFirst();
159+
headers.ifPresent(headersProp -> {
160+
final JsonValue jsonValue = headersProp.getValue();
161+
if (jsonValue != null) {
162+
endpointConfig.headers = new Gson().<Map<String, Object>>fromJson(jsonValue.getText(), Map.class);
163+
}
164+
});
165+
166+
return new GraphQLConfigVariableAwareEndpoint(endpointConfig, urlJsonProperty.getProject());
167+
168+
} catch (Exception e) {
169+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Configuration Error", e.getMessage(), NotificationType.ERROR), urlJsonProperty.getProject());
170+
}
171+
return null;
172+
}
173+
174+
@Override
175+
public void collectSlowLineMarkers(@NotNull List<PsiElement> elements, @NotNull Collection<LineMarkerInfo> result) {
176+
177+
}
178+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2018-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
package com.intellij.lang.jsgraphql.ide.editor;
9+
10+
import com.google.gson.Gson;
11+
import com.intellij.ide.actions.CreateFileAction;
12+
import com.intellij.ide.impl.DataManagerImpl;
13+
import com.intellij.notification.Notification;
14+
import com.intellij.notification.NotificationType;
15+
import com.intellij.notification.Notifications;
16+
import com.intellij.openapi.actionSystem.ActionManager;
17+
import com.intellij.openapi.actionSystem.ActionPlaces;
18+
import com.intellij.openapi.actionSystem.AnAction;
19+
import com.intellij.openapi.actionSystem.AnActionEvent;
20+
import com.intellij.openapi.application.ApplicationManager;
21+
import com.intellij.openapi.editor.Editor;
22+
import com.intellij.openapi.fileEditor.FileEditor;
23+
import com.intellij.openapi.fileEditor.FileEditorManager;
24+
import com.intellij.openapi.fileEditor.TextEditor;
25+
import com.intellij.openapi.project.Project;
26+
import com.intellij.openapi.vfs.VirtualFile;
27+
import com.intellij.psi.PsiDirectory;
28+
import com.intellij.psi.impl.file.PsiDirectoryFactory;
29+
import graphql.language.Document;
30+
import graphql.schema.idl.SchemaPrinter;
31+
import org.apache.commons.lang.StringUtils;
32+
33+
import java.io.IOException;
34+
import java.util.Date;
35+
import java.util.Map;
36+
37+
public class GraphQLIntrospectionHelper {
38+
39+
@SuppressWarnings("unchecked")
40+
static String printIntrospectionJsonAsGraphQL(String introspectionJson) {
41+
Map<String, Object> introspectionAsMap = new Gson().fromJson(introspectionJson, Map.class);
42+
if (!introspectionAsMap.containsKey("__schema")) {
43+
// possibly a full query result
44+
if (introspectionAsMap.containsKey("errors")) {
45+
throw new IllegalArgumentException("Introspection query returned errors: " + new Gson().toJson(introspectionAsMap.get("errors")));
46+
}
47+
if (!introspectionAsMap.containsKey("data")) {
48+
throw new IllegalArgumentException("Expected data key to be present in query result. Got keys: " + introspectionAsMap.keySet());
49+
}
50+
introspectionAsMap = (Map<String, Object>) introspectionAsMap.get("data");
51+
if (!introspectionAsMap.containsKey("__schema")) {
52+
throw new IllegalArgumentException("Expected __schema key to be present in query result data. Got keys: " + introspectionAsMap.keySet());
53+
}
54+
}
55+
final Document schemaDefinition = new GraphQLIntrospectionResultToSchema().createSchemaDefinition(introspectionAsMap);
56+
return new SchemaPrinter().print(schemaDefinition);
57+
}
58+
59+
60+
static void createOrUpdateIntrospectionSDLFile(String schemaAsSDL, VirtualFile introspectionSourceFile, String outputFileName, Project project) {
61+
ApplicationManager.getApplication().runWriteAction(() -> {
62+
try {
63+
final String schemaAsSDLWithHeader = "# This file was generated based on \"" + introspectionSourceFile.getName() + "\" at " + new Date() + ". Do not edit manually.\n\n" + schemaAsSDL;
64+
String relativeOutputFileName = StringUtils.replaceChars(outputFileName, '\\', '/');
65+
VirtualFile outputFile = introspectionSourceFile.getParent().findFileByRelativePath(relativeOutputFileName);
66+
if (outputFile == null) {
67+
PsiDirectory directory = PsiDirectoryFactory.getInstance(project).createDirectory(introspectionSourceFile.getParent());
68+
CreateFileAction.MkDirs dirs = new CreateFileAction.MkDirs(relativeOutputFileName, directory);
69+
outputFile = dirs.directory.getVirtualFile().createChildData(introspectionSourceFile, dirs.newName);
70+
}
71+
final FileEditor fileEditor = FileEditorManager.getInstance(project).openFile(outputFile, true, true)[0];
72+
setEditorTextAndFormatLines(schemaAsSDLWithHeader, fileEditor);
73+
} catch (IOException ioe) {
74+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL IO Error", "Unable to create file '" + outputFileName + "' in directory '" + introspectionSourceFile.getParent().getPath() + "': " + ioe.getMessage(), NotificationType.ERROR));
75+
}
76+
});
77+
}
78+
79+
static void setEditorTextAndFormatLines(String text, FileEditor fileEditor) {
80+
if (fileEditor instanceof TextEditor) {
81+
final Editor editor = ((TextEditor) fileEditor).getEditor();
82+
editor.getDocument().setText(text);
83+
AnAction reformatCode = ActionManager.getInstance().getAction("ReformatCode");
84+
if (reformatCode != null) {
85+
final AnActionEvent actionEvent = AnActionEvent.createFromDataContext(
86+
ActionPlaces.UNKNOWN,
87+
null,
88+
new DataManagerImpl.MyDataContext(editor.getComponent())
89+
);
90+
reformatCode.actionPerformed(actionEvent);
91+
}
92+
93+
}
94+
}
95+
96+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2018-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
package com.intellij.lang.jsgraphql.ide.editor;
9+
10+
import com.intellij.codeHighlighting.Pass;
11+
import com.intellij.codeInsight.daemon.LineMarkerInfo;
12+
import com.intellij.codeInsight.daemon.LineMarkerProvider;
13+
import com.intellij.icons.AllIcons;
14+
import com.intellij.json.psi.JsonArray;
15+
import com.intellij.json.psi.JsonObject;
16+
import com.intellij.json.psi.JsonProperty;
17+
import com.intellij.notification.Notification;
18+
import com.intellij.notification.NotificationType;
19+
import com.intellij.notification.Notifications;
20+
import com.intellij.openapi.editor.markup.GutterIconRenderer;
21+
import com.intellij.openapi.project.Project;
22+
import com.intellij.openapi.vfs.VirtualFile;
23+
import com.intellij.psi.PsiElement;
24+
import com.intellij.psi.util.PsiTreeUtil;
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
27+
28+
import java.util.Collection;
29+
import java.util.List;
30+
31+
import static com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper.createOrUpdateIntrospectionSDLFile;
32+
import static com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper.printIntrospectionJsonAsGraphQL;
33+
34+
/**
35+
* Line marker which shows an action to turn a GraphQL Introspection JSON result into a GraphQL schema expressed in GraphQL SDL.
36+
*/
37+
public class GraphQLIntrospectionJsonToSDLLineMarkerProvider implements LineMarkerProvider {
38+
@Nullable
39+
@Override
40+
@SuppressWarnings(value = "unchecked")
41+
public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) {
42+
if (element instanceof JsonProperty) {
43+
if (PsiTreeUtil.getParentOfType(element, JsonProperty.class) == null) {
44+
// top level property
45+
final JsonProperty jsonProperty = (JsonProperty) element;
46+
final String propertyName = jsonProperty.getName();
47+
if ("__schema".equals(propertyName) && jsonProperty.getValue() instanceof JsonObject) {
48+
for (JsonProperty property : ((JsonObject) jsonProperty.getValue()).getPropertyList()) {
49+
if ("types".equals(property.getName()) && property.getValue() instanceof JsonArray) {
50+
// likely a GraphQL schema with a { __schema: { types: [] } }
51+
return new LineMarkerInfo<>(jsonProperty, jsonProperty.getTextRange(), AllIcons.General.Run, Pass.UPDATE_ALL, o -> "Generate GraphQL SDL schema file", (e, elt) -> {
52+
try {
53+
final String introspectionJson = element.getContainingFile().getText();
54+
final String schemaAsSDL = printIntrospectionJsonAsGraphQL(introspectionJson);
55+
56+
final VirtualFile jsonFile = element.getContainingFile().getVirtualFile();
57+
final String outputFileName = jsonFile.getName() + ".graphql";
58+
final Project project = element.getProject();
59+
60+
createOrUpdateIntrospectionSDLFile(schemaAsSDL, jsonFile, outputFileName, project);
61+
62+
} catch (Exception exception) {
63+
Notifications.Bus.notify(new Notification("GraphQL", "Unable to create GraphQL SDL", exception.getMessage(), NotificationType.ERROR));
64+
}
65+
}, GutterIconRenderer.Alignment.CENTER);
66+
}
67+
}
68+
}
69+
}
70+
}
71+
return null;
72+
}
73+
74+
@Override
75+
public void collectSlowLineMarkers(@NotNull List<PsiElement> elements, @NotNull Collection<LineMarkerInfo> result) {
76+
77+
}
78+
}

0 commit comments

Comments
 (0)