Skip to content

Commit a784387

Browse files
committed
Various improvements to introspection dev workflow (#164)
- Fixed missing scalars when converting introspection JSON to SDL - Added support for JSON file as schemaPath in .graphqlconfig - Introduced re-run introspection action to easily update local schema after server changes
1 parent 648af46 commit a784387

17 files changed

+321
-63
lines changed

resources/META-INF/plugin.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
<projectService serviceInterface="com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService" serviceImplementation="com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService" />
6464
<projectService serviceInterface="com.intellij.lang.jsgraphql.GraphQLSettings" serviceImplementation="com.intellij.lang.jsgraphql.GraphQLSettings" />
6565
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigManager" serviceImplementation="com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigManager" />
66+
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper" serviceImplementation="com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper" />
6667
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigGlobMatcher" serviceImplementation="com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigGlobMatcherImpl" />
6768
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.GraphQLRelayModernAnnotationFilter" serviceImplementation="com.intellij.lang.jsgraphql.ide.GraphQLRelayModernAnnotationFilter" />
6869

@@ -343,6 +344,8 @@
343344
<keyboard-shortcut first-keystroke="meta ENTER" keymap="Mac OS X 10.5+"/>
344345
</action>
345346

347+
<action class="com.intellij.lang.jsgraphql.ide.editor.GraphQLRerunLatestIntrospectionAction" id="com.intellij.lang.jsgraphql.ide.editor.GraphQLRerunLatestIntrospectionAction" />
348+
346349
</actions>
347350

348351
</idea-plugin>

src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectEndpointUrlLineMarkerProvider.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,6 @@ public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) {
5555
final JsonProperty jsonProperty = (JsonProperty) element;
5656
final Ref<String> urlRef = Ref.create();
5757
if (isEndpointUrl(jsonProperty, urlRef) && !hasErrors(jsonProperty.getContainingFile())) {
58-
59-
final String currentSchemaPath = getSchemaPath(jsonProperty, false);
60-
if (currentSchemaPath != null && currentSchemaPath.endsWith(".json")) {
61-
// schema is based on JSON Introspection result, so doesn't make sense to show the line marker
62-
return null;
63-
}
64-
6558
return new LineMarkerInfo<>(jsonProperty, jsonProperty.getTextRange(), AllIcons.General.Run, Pass.UPDATE_ALL, o -> "Run introspection query to generate GraphQL SDL schema file", (evt, jsonUrl) -> {
6659

6760
String introspectionUtl;
@@ -83,7 +76,7 @@ public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) {
8376
final Project project = element.getProject();
8477
final VirtualFile introspectionSourceFile = element.getContainingFile().getVirtualFile();
8578

86-
GraphQLIntrospectionHelper.performIntrospectionQueryAndUpdateSchemaPathFile(project, endpoint, schemaPath, introspectionSourceFile);
79+
GraphQLIntrospectionHelper.getService(project).performIntrospectionQueryAndUpdateSchemaPathFile(project, endpoint, schemaPath, introspectionSourceFile);
8780

8881
}, GutterIconRenderer.Alignment.CENTER);
8982
}
@@ -158,12 +151,14 @@ private String getSchemaPath(JsonProperty urlElement, boolean showNotificationOn
158151
private GraphQLConfigVariableAwareEndpoint getEndpoint(String url, JsonProperty urlJsonProperty) {
159152
try {
160153

161-
final GraphQLConfigEndpoint endpointConfig = new GraphQLConfigEndpoint(null, "", url);
162-
163154
// if the endpoint is just the url string, headers are not supported
164155
final JsonProperty parent = PsiTreeUtil.getParentOfType(urlJsonProperty, JsonProperty.class);
165156
final boolean supportsHeaders = parent != null && !"endpoints".equals(parent.getName());
166157

158+
final String name = supportsHeaders ? parent.getName() : url;
159+
160+
final GraphQLConfigEndpoint endpointConfig = new GraphQLConfigEndpoint(null, name, url);
161+
167162
if (supportsHeaders) {
168163
final Stream<JsonProperty> jsonPropertyStream = PsiTreeUtil.getChildrenOfTypeAsList(urlJsonProperty.getParent(), JsonProperty.class).stream();
169164
final Optional<JsonProperty> headers = jsonPropertyStream.filter(p -> "headers".equals(p.getName())).findFirst();

src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionHelper.java

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
*/
88
package com.intellij.lang.jsgraphql.ide.editor;
99

10+
import com.google.common.collect.Sets;
1011
import com.google.gson.Gson;
1112
import com.intellij.ide.actions.CreateFileAction;
1213
import com.intellij.ide.impl.DataManagerImpl;
1314
import com.intellij.lang.jsgraphql.GraphQLSettings;
1415
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigManager;
1516
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.model.GraphQLConfigEndpoint;
1617
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.model.GraphQLConfigVariableAwareEndpoint;
18+
import com.intellij.lang.jsgraphql.schema.GraphQLSchemaKeys;
1719
import com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService;
1820
import com.intellij.notification.Notification;
1921
import com.intellij.notification.NotificationAction;
@@ -24,17 +26,23 @@
2426
import com.intellij.openapi.actionSystem.AnAction;
2527
import com.intellij.openapi.actionSystem.AnActionEvent;
2628
import com.intellij.openapi.application.ApplicationManager;
29+
import com.intellij.openapi.components.ServiceManager;
2730
import com.intellij.openapi.editor.Editor;
2831
import com.intellij.openapi.fileEditor.FileEditor;
2932
import com.intellij.openapi.fileEditor.FileEditorManager;
3033
import com.intellij.openapi.fileEditor.TextEditor;
34+
import com.intellij.openapi.progress.ProgressIndicator;
3135
import com.intellij.openapi.progress.ProgressManager;
36+
import com.intellij.openapi.progress.Task;
3237
import com.intellij.openapi.project.Project;
3338
import com.intellij.openapi.vfs.VirtualFile;
3439
import com.intellij.psi.PsiDirectory;
3540
import com.intellij.psi.impl.file.PsiDirectoryFactory;
3641
import graphql.introspection.IntrospectionQuery;
42+
import graphql.language.Description;
3743
import graphql.language.Document;
44+
import graphql.language.Node;
45+
import graphql.language.ScalarTypeDefinition;
3846
import graphql.schema.idl.SchemaPrinter;
3947
import org.apache.commons.httpclient.HttpClient;
4048
import org.apache.commons.httpclient.methods.PostMethod;
@@ -49,12 +57,25 @@
4957
import java.util.Date;
5058
import java.util.Map;
5159
import java.util.Optional;
60+
import java.util.Set;
5261

5362
import static com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService.setHeadersFromOptions;
5463

5564
public class GraphQLIntrospectionHelper {
5665

57-
public static void performIntrospectionQueryAndUpdateSchemaPathFile(Project project, GraphQLConfigEndpoint endpoint) {
66+
private GraphQLIntrospectionTask latestIntrospection = null;
67+
68+
public static GraphQLIntrospectionHelper getService(@NotNull Project project) {
69+
return ServiceManager.getService(project, GraphQLIntrospectionHelper.class);
70+
}
71+
72+
public GraphQLIntrospectionHelper(Project project) {
73+
if (project != null) {
74+
project.getMessageBus().connect().subscribe(GraphQLConfigManager.TOPIC, () -> latestIntrospection = null);
75+
}
76+
}
77+
78+
public void performIntrospectionQueryAndUpdateSchemaPathFile(Project project, GraphQLConfigEndpoint endpoint) {
5879
final VirtualFile configFile = GraphQLConfigManager.getService(project).getClosestConfigFile(endpoint.configPackageSet.getConfigBaseDir());
5980
if (configFile != null) {
6081
final String schemaPath = endpoint.configPackageSet.getConfigData().schemaPath;
@@ -65,12 +86,14 @@ public static void performIntrospectionQueryAndUpdateSchemaPathFile(Project proj
6586
return;
6687
}
6788

68-
GraphQLIntrospectionHelper.performIntrospectionQueryAndUpdateSchemaPathFile(project, new GraphQLConfigVariableAwareEndpoint(endpoint, project), schemaPath, configFile);
89+
performIntrospectionQueryAndUpdateSchemaPathFile(project, new GraphQLConfigVariableAwareEndpoint(endpoint, project), schemaPath, configFile);
6990
}
7091

7192
}
7293

73-
public static void performIntrospectionQueryAndUpdateSchemaPathFile(Project project, GraphQLConfigVariableAwareEndpoint endpoint, String schemaPath, VirtualFile introspectionSourceFile) {
94+
public void performIntrospectionQueryAndUpdateSchemaPathFile(Project project, GraphQLConfigVariableAwareEndpoint endpoint, String schemaPath, VirtualFile introspectionSourceFile) {
95+
96+
latestIntrospection = new GraphQLIntrospectionTask(endpoint, () -> performIntrospectionQueryAndUpdateSchemaPathFile(project, endpoint, schemaPath, introspectionSourceFile));
7497

7598
final NotificationAction retry = new NotificationAction("Retry") {
7699

@@ -99,34 +122,37 @@ public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification noti
99122

100123
setHeadersFromOptions(endpoint, method);
101124

102-
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
103-
ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true);
104-
try {
105-
httpClient.executeMethod(method);
106-
final String responseJson = Optional.ofNullable(method.getResponseBodyAsString()).orElse("");
107-
ApplicationManager.getApplication().invokeLater(() -> {
108-
try {
109-
JSGraphQLLanguageUIProjectService.getService(project).showQueryResult(responseJson);
110-
final String schemaAsSDL = printIntrospectionJsonAsGraphQL(responseJson);
111-
createOrUpdateIntrospectionSDLFile(schemaAsSDL, introspectionSourceFile, schemaPath, project);
112-
} catch (Exception e) {
113-
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Introspection Error", e.getMessage(), NotificationType.WARNING).addAction(retry), project);
114-
}
115-
});
116-
} catch (IOException e) {
117-
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.WARNING).addAction(retry), project);
125+
final Task.Backgroundable task = new Task.Backgroundable(project, "Executing GraphQL Introspection Query", false) {
126+
@Override
127+
public void run(@NotNull ProgressIndicator indicator) {
128+
indicator.setIndeterminate(true);
129+
try {
130+
httpClient.executeMethod(method);
131+
final String responseJson = Optional.ofNullable(method.getResponseBodyAsString()).orElse("");
132+
ApplicationManager.getApplication().invokeLater(() -> {
133+
try {
134+
JSGraphQLLanguageUIProjectService.getService(project).showQueryResult(responseJson, JSGraphQLLanguageUIProjectService.QueryResultDisplay.ON_ERRORS_ONLY);
135+
IntrospectionOutputFormat format = schemaPath.endsWith(".json") ? IntrospectionOutputFormat.JSON : IntrospectionOutputFormat.SDL;
136+
final String schemaText = format == IntrospectionOutputFormat.SDL ? printIntrospectionJsonAsGraphQL(responseJson) : responseJson;
137+
createOrUpdateIntrospectionOutputFile(schemaText, format, introspectionSourceFile, schemaPath, project);
138+
} catch (Exception e) {
139+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Introspection Error", e.getMessage(), NotificationType.WARNING).addAction(retry), project);
140+
}
141+
});
142+
} catch (IOException e) {
143+
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.WARNING).addAction(retry), project);
144+
}
118145
}
119-
120-
}, "Executing GraphQL Introspection Query", false, project);
121-
146+
};
147+
ProgressManager.getInstance().run(task);
122148

123149
} catch (UnsupportedEncodingException | IllegalStateException | IllegalArgumentException e) {
124150
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.ERROR).addAction(retry), project);
125151
}
126152
}
127153

128154
@SuppressWarnings("unchecked")
129-
public static String printIntrospectionJsonAsGraphQL(String introspectionJson) {
155+
public String printIntrospectionJsonAsGraphQL(String introspectionJson) {
130156
Map<String, Object> introspectionAsMap = new Gson().fromJson(introspectionJson, Map.class);
131157
if (!introspectionAsMap.containsKey("__schema")) {
132158
// possibly a full query result
@@ -142,30 +168,73 @@ public static String printIntrospectionJsonAsGraphQL(String introspectionJson) {
142168
}
143169
}
144170
final Document schemaDefinition = new GraphQLIntrospectionResultToSchema().createSchemaDefinition(introspectionAsMap);
145-
return new SchemaPrinter().print(schemaDefinition);
171+
final SchemaPrinter.Options options = SchemaPrinter.Options.defaultOptions().includeScalarTypes(true).includeSchemaDefintion(true);
172+
final StringBuilder sb = new StringBuilder(new SchemaPrinter(options).print(schemaDefinition));
173+
174+
// graphql-java currently appears to discard custom scalars as part of the schema that the printer constructs, so include them manually here
175+
final Set<String> knownScalars = Sets.newHashSet();
176+
for (Node definition : schemaDefinition.getChildren()) {
177+
if (definition instanceof ScalarTypeDefinition) {
178+
final ScalarTypeDefinition scalarTypeDefinition = (ScalarTypeDefinition) definition;
179+
String scalarName = scalarTypeDefinition.getName();
180+
if (knownScalars.add(scalarName)) {
181+
sb.append("\n");
182+
final Description description = scalarTypeDefinition.getDescription();
183+
if (description != null) {
184+
if (description.isMultiLine()) {
185+
sb.append("\"\"\"").append(description.getContent()).append("\"\"\"");
186+
} else {
187+
sb.append("\"").append(description.getContent()).append("\"");
188+
}
189+
sb.append("\n");
190+
}
191+
sb.append("scalar ").append(scalarName);
192+
}
193+
}
194+
}
195+
return sb.toString();
146196
}
147197

198+
public GraphQLIntrospectionTask getLatestIntrospection() {
199+
return latestIntrospection;
200+
}
201+
202+
enum IntrospectionOutputFormat {
203+
JSON,
204+
SDL
205+
}
148206

149-
static void createOrUpdateIntrospectionSDLFile(String schemaAsSDL, VirtualFile introspectionSourceFile, String outputFileName, Project project) {
207+
void createOrUpdateIntrospectionOutputFile(String schemaText, IntrospectionOutputFormat format, VirtualFile introspectionSourceFile, String outputFileName, Project project) {
150208
ApplicationManager.getApplication().runWriteAction(() -> {
151209
try {
152-
final String schemaAsSDLWithHeader = "# This file was generated based on \"" + introspectionSourceFile.getName() + "\" at " + new Date() + ". Do not edit manually.\n\n" + schemaAsSDL;
210+
final String header;
211+
switch (format) {
212+
case SDL:
213+
header = "# This file was generated based on \"" + introspectionSourceFile.getName() + "\" at " + new Date() + ". Do not edit manually.\n\n";
214+
break;
215+
case JSON:
216+
header = "";
217+
break;
218+
default:
219+
throw new IllegalArgumentException("unsupported output format: " + format);
220+
}
153221
String relativeOutputFileName = StringUtils.replaceChars(outputFileName, '\\', '/');
154222
VirtualFile outputFile = introspectionSourceFile.getParent().findFileByRelativePath(relativeOutputFileName);
155223
if (outputFile == null) {
156224
PsiDirectory directory = PsiDirectoryFactory.getInstance(project).createDirectory(introspectionSourceFile.getParent());
157225
CreateFileAction.MkDirs dirs = new CreateFileAction.MkDirs(relativeOutputFileName, directory);
158226
outputFile = dirs.directory.getVirtualFile().createChildData(introspectionSourceFile, dirs.newName);
159227
}
228+
outputFile.putUserData(GraphQLSchemaKeys.IS_GRAPHQL_INTROSPECTION_JSON, true);
160229
final FileEditor fileEditor = FileEditorManager.getInstance(project).openFile(outputFile, true, true)[0];
161-
setEditorTextAndFormatLines(schemaAsSDLWithHeader, fileEditor);
230+
setEditorTextAndFormatLines(header + schemaText, fileEditor);
162231
} catch (IOException ioe) {
163232
Notifications.Bus.notify(new Notification("GraphQL", "GraphQL IO Error", "Unable to create file '" + outputFileName + "' in directory '" + introspectionSourceFile.getParent().getPath() + "': " + ioe.getMessage(), NotificationType.ERROR));
164233
}
165234
});
166235
}
167236

168-
static void setEditorTextAndFormatLines(String text, FileEditor fileEditor) {
237+
void setEditorTextAndFormatLines(String text, FileEditor fileEditor) {
169238
if (fileEditor instanceof TextEditor) {
170239
final Editor editor = ((TextEditor) fileEditor).getEditor();
171240
editor.getDocument().setText(text);

0 commit comments

Comments
 (0)