vscode-apollo 1.19.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.circleci/config.yml +91 -0
  4. package/.eslintrc.js +10 -0
  5. package/.git-blame-ignore-revs +2 -0
  6. package/.gitattributes +1 -0
  7. package/.github/workflows/release.yml +95 -0
  8. package/.gitleaks.toml +26 -0
  9. package/.nvmrc +1 -0
  10. package/.prettierrc +5 -0
  11. package/.vscode/launch.json +66 -0
  12. package/.vscode/settings.json +16 -0
  13. package/.vscode/tasks.json +60 -0
  14. package/.vscodeignore +28 -1
  15. package/CHANGELOG.md +250 -1
  16. package/CODEOWNERS +4 -0
  17. package/LICENSE +2 -2
  18. package/README.md +104 -55
  19. package/codegen.yml +12 -0
  20. package/graphql.configuration.json +5 -1
  21. package/images/IconRun.svg +8 -0
  22. package/images/marketplace/apollo-wordmark.png +0 -0
  23. package/jest.config.ts +21 -0
  24. package/jest.e2e.config.js +17 -0
  25. package/package.json +102 -23
  26. package/renovate.json +30 -0
  27. package/sampleWorkspace/clientSchema/apollo.config.cjs +10 -0
  28. package/sampleWorkspace/clientSchema/src/clientSchema.js +16 -0
  29. package/sampleWorkspace/clientSchema/src/test.js +18 -0
  30. package/sampleWorkspace/fixtures/starwarsSchema.graphql +299 -0
  31. package/sampleWorkspace/httpSchema/apollo.config.ts +8 -0
  32. package/sampleWorkspace/httpSchema/src/test.js +9 -0
  33. package/sampleWorkspace/localSchema/apollo.config.js +8 -0
  34. package/sampleWorkspace/localSchema/src/test.js +8 -0
  35. package/sampleWorkspace/localSchemaArray/apollo.config.js +12 -0
  36. package/sampleWorkspace/localSchemaArray/planets.graphql +20 -0
  37. package/sampleWorkspace/localSchemaArray/src/test.js +12 -0
  38. package/sampleWorkspace/sampleWorkspace.code-workspace +20 -0
  39. package/sampleWorkspace/spotifyGraph/apollo.config.mjs +5 -0
  40. package/sampleWorkspace/spotifyGraph/src/test.js +11 -0
  41. package/src/__e2e__/mockServer.js +117 -0
  42. package/src/__e2e__/mocks.js +13094 -0
  43. package/src/__e2e__/run.js +23 -0
  44. package/src/__e2e__/runTests.js +44 -0
  45. package/src/__e2e__/setup.js +1 -0
  46. package/src/__e2e__/vscode-environment.js +16 -0
  47. package/src/__e2e__/vscode.js +1 -0
  48. package/src/__mocks__/fs.js +3 -0
  49. package/src/__tests__/statusBar.test.ts +8 -7
  50. package/src/build.js +57 -0
  51. package/src/debug.ts +2 -5
  52. package/src/env/index.ts +1 -0
  53. package/src/env/typescript-utility-types.ts +2 -0
  54. package/src/extension.ts +265 -170
  55. package/src/language-server/__e2e__/clientSchema.e2e.ts +147 -0
  56. package/src/language-server/__e2e__/httpSchema.e2e.ts +21 -0
  57. package/src/language-server/__e2e__/localSchema.e2e.ts +25 -0
  58. package/src/language-server/__e2e__/localSchemaArray.e2e.ts +31 -0
  59. package/src/language-server/__e2e__/studioGraph.e2e.ts +65 -0
  60. package/src/language-server/__e2e__/utils.ts +151 -0
  61. package/src/language-server/__tests__/diagnostics.test.ts +86 -0
  62. package/src/language-server/__tests__/document.test.ts +187 -0
  63. package/src/language-server/__tests__/fileSet.test.ts +46 -0
  64. package/src/language-server/__tests__/fixtures/starwarsSchema.ts +1917 -0
  65. package/src/language-server/config/__tests__/config.ts +54 -0
  66. package/src/language-server/config/__tests__/loadConfig.ts +384 -0
  67. package/src/language-server/config/__tests__/utils.ts +99 -0
  68. package/src/language-server/config/config.ts +284 -0
  69. package/src/language-server/config/index.ts +3 -0
  70. package/src/language-server/config/loadConfig.ts +101 -0
  71. package/src/language-server/config/utils.ts +45 -0
  72. package/src/language-server/diagnostics.ts +118 -0
  73. package/src/language-server/document.ts +277 -0
  74. package/src/language-server/engine/index.ts +123 -0
  75. package/src/language-server/engine/operations/frontendUrlRoot.ts +15 -0
  76. package/src/language-server/engine/operations/schemaTagsAndFieldStats.ts +32 -0
  77. package/src/language-server/errors/__tests__/NoMissingClientDirectives.test.ts +225 -0
  78. package/src/language-server/errors/logger.ts +58 -0
  79. package/src/language-server/errors/validation.ts +274 -0
  80. package/src/language-server/fileSet.ts +63 -0
  81. package/src/language-server/format.ts +48 -0
  82. package/src/language-server/graphqlTypes.ts +16741 -0
  83. package/src/language-server/index.ts +28 -0
  84. package/src/language-server/languageProvider.ts +795 -0
  85. package/src/language-server/loadingHandler.ts +47 -0
  86. package/src/language-server/project/base.ts +406 -0
  87. package/src/language-server/project/client.ts +568 -0
  88. package/src/language-server/project/defaultClientSchema.ts +70 -0
  89. package/src/language-server/providers/schema/__tests__/file.ts +191 -0
  90. package/src/language-server/providers/schema/base.ts +15 -0
  91. package/src/language-server/providers/schema/endpoint.ts +138 -0
  92. package/src/language-server/providers/schema/engine.ts +204 -0
  93. package/src/language-server/providers/schema/file.ts +176 -0
  94. package/src/language-server/providers/schema/index.ts +59 -0
  95. package/src/language-server/server.ts +274 -0
  96. package/src/language-server/typings/graphql.d.ts +27 -0
  97. package/src/language-server/utilities/__tests__/graphql.test.ts +399 -0
  98. package/src/language-server/utilities/__tests__/uri.ts +55 -0
  99. package/src/language-server/utilities/debouncer.ts +8 -0
  100. package/src/language-server/utilities/debug.ts +90 -0
  101. package/src/language-server/utilities/graphql.ts +433 -0
  102. package/src/language-server/utilities/index.ts +3 -0
  103. package/src/language-server/utilities/source.ts +182 -0
  104. package/src/language-server/utilities/uri.ts +19 -0
  105. package/src/language-server/workspace.ts +254 -0
  106. package/src/languageServerClient.ts +22 -15
  107. package/src/messages.ts +75 -0
  108. package/src/statusBar.ts +5 -5
  109. package/src/tools/__tests__/buildServiceDefinition.test.ts +491 -0
  110. package/src/tools/__tests__/snapshotSerializers/astSerializer.ts +19 -0
  111. package/src/tools/__tests__/snapshotSerializers/graphQLTypeSerializer.ts +14 -0
  112. package/src/tools/buildServiceDefinition.ts +241 -0
  113. package/src/tools/index.ts +6 -0
  114. package/src/tools/schema/index.ts +2 -0
  115. package/src/tools/schema/resolveObject.ts +18 -0
  116. package/src/tools/schema/resolverMap.ts +23 -0
  117. package/src/tools/utilities/graphql.ts +22 -0
  118. package/src/tools/utilities/index.ts +3 -0
  119. package/src/tools/utilities/invariant.ts +5 -0
  120. package/src/tools/utilities/predicates.ts +5 -0
  121. package/src/utils.ts +7 -21
  122. package/syntaxes/graphql.dart.json +2 -4
  123. package/syntaxes/graphql.ex.json +1 -4
  124. package/syntaxes/graphql.js.json +3 -3
  125. package/syntaxes/graphql.json +13 -9
  126. package/syntaxes/graphql.lua.json +51 -0
  127. package/syntaxes/graphql.rb.json +1 -1
  128. package/tsconfig.build.json +11 -0
  129. package/tsconfig.json +22 -7
  130. package/create-server-symlink.js +0 -8
  131. package/lib/debug.d.ts +0 -11
  132. package/lib/debug.d.ts.map +0 -1
  133. package/lib/debug.js +0 -48
  134. package/lib/debug.js.map +0 -1
  135. package/lib/extension.d.ts +0 -4
  136. package/lib/extension.d.ts.map +0 -1
  137. package/lib/extension.js +0 -187
  138. package/lib/extension.js.map +0 -1
  139. package/lib/languageServerClient.d.ts +0 -4
  140. package/lib/languageServerClient.d.ts.map +0 -1
  141. package/lib/languageServerClient.js +0 -57
  142. package/lib/languageServerClient.js.map +0 -1
  143. package/lib/statusBar.d.ts +0 -24
  144. package/lib/statusBar.d.ts.map +0 -1
  145. package/lib/statusBar.js +0 -46
  146. package/lib/statusBar.js.map +0 -1
  147. package/lib/testRunner/index.d.ts +0 -3
  148. package/lib/testRunner/index.d.ts.map +0 -1
  149. package/lib/testRunner/index.js +0 -49
  150. package/lib/testRunner/index.js.map +0 -1
  151. package/lib/testRunner/jest-config.d.ts +0 -14
  152. package/lib/testRunner/jest-config.d.ts.map +0 -1
  153. package/lib/testRunner/jest-config.js +0 -18
  154. package/lib/testRunner/jest-config.js.map +0 -1
  155. package/lib/testRunner/jest-vscode-environment.d.ts +0 -2
  156. package/lib/testRunner/jest-vscode-environment.d.ts.map +0 -1
  157. package/lib/testRunner/jest-vscode-environment.js +0 -19
  158. package/lib/testRunner/jest-vscode-environment.js.map +0 -1
  159. package/lib/testRunner/jest-vscode-framework-setup.d.ts +0 -1
  160. package/lib/testRunner/jest-vscode-framework-setup.d.ts.map +0 -1
  161. package/lib/testRunner/jest-vscode-framework-setup.js +0 -3
  162. package/lib/testRunner/jest-vscode-framework-setup.js.map +0 -1
  163. package/lib/testRunner/vscode-test-script.d.ts +0 -2
  164. package/lib/testRunner/vscode-test-script.d.ts.map +0 -1
  165. package/lib/testRunner/vscode-test-script.js +0 -23
  166. package/lib/testRunner/vscode-test-script.js.map +0 -1
  167. package/lib/utils.d.ts +0 -18
  168. package/lib/utils.d.ts.map +0 -1
  169. package/lib/utils.js +0 -52
  170. package/lib/utils.js.map +0 -1
  171. package/src/testRunner/README.md +0 -23
  172. package/src/testRunner/index.ts +0 -72
  173. package/src/testRunner/jest-config.ts +0 -17
  174. package/src/testRunner/jest-vscode-environment.ts +0 -25
  175. package/src/testRunner/jest-vscode-framework-setup.ts +0 -10
  176. package/src/testRunner/jest.d.ts +0 -37
  177. package/src/testRunner/vscode-test-script.ts +0 -38
  178. package/tsconfig.test.json +0 -4
  179. package/tsconfig.tsbuildinfo +0 -2486
@@ -0,0 +1,277 @@
1
+ import { parse, Source, DocumentNode } from "graphql";
2
+ import { SourceLocation, getLocation } from "graphql/language/location";
3
+
4
+ import {
5
+ TextDocument,
6
+ Position,
7
+ Diagnostic,
8
+ DiagnosticSeverity,
9
+ } from "vscode-languageserver/node";
10
+
11
+ import { getRange as rangeOfTokenAtLocation } from "graphql-language-service";
12
+
13
+ import {
14
+ positionFromSourceLocation,
15
+ rangeInContainingDocument,
16
+ } from "./utilities/source";
17
+
18
+ export class GraphQLDocument {
19
+ ast?: DocumentNode;
20
+ syntaxErrors: Diagnostic[] = [];
21
+
22
+ constructor(public source: Source) {
23
+ try {
24
+ this.ast = parse(source);
25
+ } catch (error: any) {
26
+ // Don't add syntax errors when GraphQL has been commented out
27
+ if (maybeCommentedOut(source.body)) return;
28
+
29
+ // A GraphQL syntax error only has a location and no node, because we don't have an AST
30
+ // So we use the online parser to get the range of the token at that location
31
+ const range = rangeInContainingDocument(
32
+ source,
33
+ rangeOfTokenAtLocation(error.locations[0], source.body),
34
+ );
35
+ this.syntaxErrors.push({
36
+ severity: DiagnosticSeverity.Error,
37
+ message: error.message,
38
+ source: "GraphQL: Syntax",
39
+ range,
40
+ });
41
+ }
42
+ }
43
+
44
+ containsPosition(position: Position): boolean {
45
+ if (position.line < this.source.locationOffset.line - 1) return false;
46
+ const end = positionFromSourceLocation(
47
+ this.source,
48
+ getLocation(this.source, this.source.body.length),
49
+ );
50
+ return position.line <= end.line;
51
+ }
52
+ }
53
+
54
+ export function extractGraphQLDocuments(
55
+ document: TextDocument,
56
+ tagName: string = "gql",
57
+ ): GraphQLDocument[] | null {
58
+ switch (document.languageId) {
59
+ case "graphql":
60
+ return [
61
+ new GraphQLDocument(new Source(document.getText(), document.uri)),
62
+ ];
63
+ case "javascript":
64
+ case "javascriptreact":
65
+ case "typescript":
66
+ case "typescriptreact":
67
+ case "vue":
68
+ case "svelte":
69
+ return extractGraphQLDocumentsFromJSTemplateLiterals(document, tagName);
70
+ case "python":
71
+ return extractGraphQLDocumentsFromPythonStrings(document, tagName);
72
+ case "ruby":
73
+ return extractGraphQLDocumentsFromRubyStrings(document, tagName);
74
+ case "dart":
75
+ return extractGraphQLDocumentsFromDartStrings(document, tagName);
76
+ case "reason":
77
+ return extractGraphQLDocumentsFromReasonStrings(document, tagName);
78
+ case "elixir":
79
+ return extractGraphQLDocumentsFromElixirStrings(document, tagName);
80
+ default:
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function extractGraphQLDocumentsFromJSTemplateLiterals(
86
+ document: TextDocument,
87
+ tagName: string,
88
+ ): GraphQLDocument[] | null {
89
+ const text = document.getText();
90
+
91
+ const documents: GraphQLDocument[] = [];
92
+
93
+ const regExp = new RegExp(
94
+ `(?:${tagName}(?:\\s|\\()*\`|\`#graphql)([\\s\\S]+?)\`\\)?`,
95
+ "gm",
96
+ );
97
+
98
+ let result;
99
+ while ((result = regExp.exec(text)) !== null) {
100
+ const contents = replacePlaceholdersWithWhiteSpace(result[1]);
101
+ const position = document.positionAt(result.index + (tagName.length + 1));
102
+ const locationOffset: SourceLocation = {
103
+ line: position.line + 1,
104
+ column: position.character + 1,
105
+ };
106
+ const source = new Source(contents, document.uri, locationOffset);
107
+ documents.push(new GraphQLDocument(source));
108
+ }
109
+
110
+ if (documents.length < 1) return null;
111
+
112
+ return documents;
113
+ }
114
+
115
+ function extractGraphQLDocumentsFromPythonStrings(
116
+ document: TextDocument,
117
+ tagName: string,
118
+ ): GraphQLDocument[] | null {
119
+ const text = document.getText();
120
+
121
+ const documents: GraphQLDocument[] = [];
122
+
123
+ const regExp = new RegExp(
124
+ `\\b(${tagName}\\s*\\(\\s*[bfru]*("(?:"")?|'(?:'')?))([\\s\\S]+?)\\2\\s*\\)`,
125
+ "gm",
126
+ );
127
+
128
+ let result;
129
+ while ((result = regExp.exec(text)) !== null) {
130
+ const contents = replacePlaceholdersWithWhiteSpace(result[3]);
131
+ const position = document.positionAt(result.index + result[1].length);
132
+ const locationOffset: SourceLocation = {
133
+ line: position.line + 1,
134
+ column: position.character + 1,
135
+ };
136
+ const source = new Source(contents, document.uri, locationOffset);
137
+ documents.push(new GraphQLDocument(source));
138
+ }
139
+
140
+ if (documents.length < 1) return null;
141
+
142
+ return documents;
143
+ }
144
+
145
+ function extractGraphQLDocumentsFromRubyStrings(
146
+ document: TextDocument,
147
+ tagName: string,
148
+ ): GraphQLDocument[] | null {
149
+ const text = document.getText();
150
+
151
+ const documents: GraphQLDocument[] = [];
152
+
153
+ const regExp = new RegExp(`(<<-${tagName})([\\s\\S]+?)${tagName}`, "gm");
154
+
155
+ let result;
156
+ while ((result = regExp.exec(text)) !== null) {
157
+ const contents = replacePlaceholdersWithWhiteSpace(result[2]);
158
+ const position = document.positionAt(result.index + result[1].length);
159
+ const locationOffset: SourceLocation = {
160
+ line: position.line + 1,
161
+ column: position.character + 1,
162
+ };
163
+ const source = new Source(contents, document.uri, locationOffset);
164
+ documents.push(new GraphQLDocument(source));
165
+ }
166
+
167
+ if (documents.length < 1) return null;
168
+
169
+ return documents;
170
+ }
171
+
172
+ function extractGraphQLDocumentsFromDartStrings(
173
+ document: TextDocument,
174
+ tagName: string,
175
+ ): GraphQLDocument[] | null {
176
+ const text = document.getText();
177
+
178
+ const documents: GraphQLDocument[] = [];
179
+
180
+ const regExp = new RegExp(
181
+ `\\b(${tagName}\\(\\s*r?("""|'''))([\\s\\S]+?)\\2\\s*\\)`,
182
+ "gm",
183
+ );
184
+
185
+ let result;
186
+ while ((result = regExp.exec(text)) !== null) {
187
+ const contents = replacePlaceholdersWithWhiteSpace(result[3]);
188
+ const position = document.positionAt(result.index + result[1].length);
189
+ const locationOffset: SourceLocation = {
190
+ line: position.line + 1,
191
+ column: position.character + 1,
192
+ };
193
+ const source = new Source(contents, document.uri, locationOffset);
194
+ documents.push(new GraphQLDocument(source));
195
+ }
196
+
197
+ if (documents.length < 1) return null;
198
+
199
+ return documents;
200
+ }
201
+
202
+ function extractGraphQLDocumentsFromReasonStrings(
203
+ document: TextDocument,
204
+ tagName: string,
205
+ ): GraphQLDocument[] | null {
206
+ const text = document.getText();
207
+
208
+ const documents: GraphQLDocument[] = [];
209
+
210
+ const reasonFileFilter = new RegExp(/(\[%(graphql|relay\.))/g);
211
+
212
+ if (!reasonFileFilter.test(text)) {
213
+ return documents;
214
+ }
215
+
216
+ const reasonRegexp = new RegExp(
217
+ /(?<=\[%(graphql|relay\.\w*)[\s\S]*{\|)[.\s\S]+?(?=\|})/gm,
218
+ );
219
+
220
+ let result;
221
+ while ((result = reasonRegexp.exec(text)) !== null) {
222
+ const contents = result[0];
223
+ const position = document.positionAt(result.index);
224
+ const locationOffset: SourceLocation = {
225
+ line: position.line + 1,
226
+ column: position.character + 1,
227
+ };
228
+ const source = new Source(contents, document.uri, locationOffset);
229
+ documents.push(new GraphQLDocument(source));
230
+ }
231
+
232
+ if (documents.length < 1) return null;
233
+
234
+ return documents;
235
+ }
236
+
237
+ function extractGraphQLDocumentsFromElixirStrings(
238
+ document: TextDocument,
239
+ tagName: string,
240
+ ): GraphQLDocument[] | null {
241
+ const text = document.getText();
242
+ const documents: GraphQLDocument[] = [];
243
+
244
+ const regExp = new RegExp(
245
+ `\\b(${tagName}\\(\\s*r?("""))([\\s\\S]+?)\\2\\s*\\)`,
246
+ "gm",
247
+ );
248
+
249
+ let result;
250
+ while ((result = regExp.exec(text)) !== null) {
251
+ const contents = replacePlaceholdersWithWhiteSpace(result[3]);
252
+ const position = document.positionAt(result.index + result[1].length);
253
+ const locationOffset: SourceLocation = {
254
+ line: position.line + 1,
255
+ column: position.character + 1,
256
+ };
257
+ const source = new Source(contents, document.uri, locationOffset);
258
+ documents.push(new GraphQLDocument(source));
259
+ }
260
+
261
+ if (documents.length < 1) return null;
262
+
263
+ return documents;
264
+ }
265
+
266
+ function replacePlaceholdersWithWhiteSpace(content: string) {
267
+ return content.replace(/\$\{([\s\S]+?)\}/gm, (match) => {
268
+ return Array(match.length).join(" ");
269
+ });
270
+ }
271
+
272
+ function maybeCommentedOut(content: string) {
273
+ return (
274
+ (content.indexOf("/*") > -1 && content.indexOf("*/") > -1) ||
275
+ content.split("//").length > 1
276
+ );
277
+ }
@@ -0,0 +1,123 @@
1
+ import { SCHEMA_TAGS_AND_FIELD_STATS } from "./operations/schemaTagsAndFieldStats";
2
+ import { FRONTEND_URL_ROOT } from "./operations/frontendUrlRoot";
3
+ import {
4
+ ApolloClient,
5
+ ApolloLink,
6
+ createHttpLink,
7
+ InMemoryCache,
8
+ } from "@apollo/client/core";
9
+ import { setContext } from "@apollo/client/link/context";
10
+ import { onError } from "@apollo/client/link/error";
11
+
12
+ export interface ClientIdentity {
13
+ name: string;
14
+ version: string;
15
+ referenceID: string;
16
+ }
17
+
18
+ export type ServiceID = string;
19
+ export type ClientID = string;
20
+ export type SchemaTag = string;
21
+ export type ServiceIDAndTag = [ServiceID, SchemaTag?];
22
+ export type ServiceSpecifier = string;
23
+ // Map from parent type name to field name to latency in ms.
24
+ export type FieldLatenciesMS = Map<string, Map<string, number | null>>;
25
+
26
+ export function noServiceError(service: string | undefined, endpoint?: string) {
27
+ return `Could not find graph ${
28
+ service ? service : ""
29
+ } from Apollo at ${endpoint}. Please check your API key and graph ID`;
30
+ }
31
+
32
+ export class ApolloEngineClient {
33
+ public readonly client: ApolloClient<any>;
34
+ public readonly query: ApolloClient<any>["query"];
35
+
36
+ constructor(
37
+ private readonly engineKey: string,
38
+ baseURL: string,
39
+ private readonly clientIdentity: ClientIdentity,
40
+ ) {
41
+ const link = ApolloLink.from([
42
+ onError(({ graphQLErrors, networkError, operation }) => {
43
+ const { result, response } = operation.getContext();
44
+ if (graphQLErrors) {
45
+ graphQLErrors.map((graphqlError) =>
46
+ console.error(`[GraphQL error]: ${graphqlError.message}`),
47
+ );
48
+ }
49
+
50
+ if (networkError) {
51
+ console.log(`[Network Error]: ${networkError}`);
52
+ }
53
+
54
+ if (response && response.status >= 400) {
55
+ console.log(`[Network Error] ${response.bodyText}`);
56
+ }
57
+ }),
58
+ createHttpLink({
59
+ uri: baseURL,
60
+ headers: {
61
+ ["x-api-key"]: this.engineKey,
62
+ ["apollo-client-name"]: this.clientIdentity.name,
63
+ ["apollo-client-reference-id"]: this.clientIdentity.referenceID,
64
+ ["apollo-client-version"]: this.clientIdentity.version,
65
+ },
66
+ }),
67
+ ]);
68
+
69
+ this.client = new ApolloClient({
70
+ link,
71
+ cache: new InMemoryCache(),
72
+ });
73
+ this.query = this.client.query.bind(this.client);
74
+ }
75
+
76
+ async loadSchemaTagsAndFieldLatencies(serviceID: string) {
77
+ const { data, errors } = await this.client.query({
78
+ query: SCHEMA_TAGS_AND_FIELD_STATS,
79
+ variables: {
80
+ id: serviceID,
81
+ },
82
+ fetchPolicy: "no-cache",
83
+ });
84
+
85
+ if (!(data && data.service && data.service.schemaTags) || errors) {
86
+ throw new Error(
87
+ errors
88
+ ? errors.map((error) => error.message).join("\n")
89
+ : "No service returned. Make sure your service name and API key match",
90
+ );
91
+ }
92
+
93
+ const schemaTags: string[] = data.service.schemaTags.map(
94
+ ({ tag }: { tag: string }) => tag,
95
+ );
96
+
97
+ const fieldLatenciesMS: FieldLatenciesMS = new Map();
98
+
99
+ data.service.stats.fieldLatencies.forEach((fieldLatency) => {
100
+ const { parentType, fieldName } = fieldLatency.groupBy;
101
+
102
+ if (!parentType || !fieldName) {
103
+ return;
104
+ }
105
+ const fieldsMap =
106
+ fieldLatenciesMS.get(parentType) ||
107
+ fieldLatenciesMS.set(parentType, new Map()).get(parentType)!;
108
+
109
+ fieldsMap.set(fieldName, fieldLatency.metrics.fieldHistogram.durationMs);
110
+ });
111
+
112
+ return { schemaTags, fieldLatenciesMS };
113
+ }
114
+
115
+ async loadFrontendUrlRoot() {
116
+ const { data } = await this.client.query({
117
+ query: FRONTEND_URL_ROOT,
118
+ fetchPolicy: "cache-first",
119
+ });
120
+
121
+ return data?.frontendUrlRoot;
122
+ }
123
+ }
@@ -0,0 +1,15 @@
1
+ import { TypedDocumentNode } from "@apollo/client/core";
2
+ import gql from "graphql-tag";
3
+ import type {
4
+ FrontendUrlRootQuery,
5
+ FrontendUrlRootQueryVariables,
6
+ } from "src/language-server/graphqlTypes";
7
+
8
+ export const FRONTEND_URL_ROOT: TypedDocumentNode<
9
+ FrontendUrlRootQuery,
10
+ FrontendUrlRootQueryVariables
11
+ > = gql`
12
+ query FrontendUrlRoot {
13
+ frontendUrlRoot
14
+ }
15
+ `;
@@ -0,0 +1,32 @@
1
+ import { TypedDocumentNode } from "@apollo/client/core";
2
+ import gql from "graphql-tag";
3
+ import type {
4
+ SchemaTagsAndFieldStatsQuery,
5
+ SchemaTagsAndFieldStatsQueryVariables,
6
+ } from "src/language-server/graphqlTypes";
7
+
8
+ export const SCHEMA_TAGS_AND_FIELD_STATS: TypedDocumentNode<
9
+ SchemaTagsAndFieldStatsQuery,
10
+ SchemaTagsAndFieldStatsQueryVariables
11
+ > = gql`
12
+ query SchemaTagsAndFieldStats($id: ID!) {
13
+ service(id: $id) {
14
+ schemaTags {
15
+ tag
16
+ }
17
+ stats(from: "-86400", to: "-0") {
18
+ fieldLatencies {
19
+ groupBy {
20
+ parentType
21
+ fieldName
22
+ }
23
+ metrics {
24
+ fieldHistogram {
25
+ durationMs(percentile: 0.95)
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ `;
@@ -0,0 +1,225 @@
1
+ import { NoMissingClientDirectives } from "../validation";
2
+ import { GraphQLClientProject } from "../../project/client";
3
+ import { basename } from "path";
4
+
5
+ import { vol } from "memfs";
6
+ import { LoadingHandler } from "../../loadingHandler";
7
+ import { ClientConfig, parseApolloConfig } from "../../config";
8
+ import { URI } from "vscode-uri";
9
+
10
+ const serviceSchema = /* GraphQL */ `
11
+ type Query {
12
+ me: User
13
+ }
14
+
15
+ type User {
16
+ name: String
17
+ friends: [User]
18
+ }
19
+ `;
20
+ const clientSchema = /* GraphQL */ `
21
+ extend type Query {
22
+ isOnline: Boolean
23
+ }
24
+ extend type User {
25
+ isLiked: Boolean
26
+ localUser: User
27
+ }
28
+ `;
29
+ const a = /* GraphQL */ `
30
+ query a {
31
+ isOnline
32
+ me {
33
+ name
34
+ foo # added field missing in service schema to ensure it doesn't throw, see https://github.com/apollographql/vscode-graphql/pull/73
35
+ localUser @client {
36
+ friends {
37
+ isLiked
38
+ }
39
+ }
40
+ friends {
41
+ name
42
+ isLiked
43
+ }
44
+ }
45
+ }
46
+ `;
47
+
48
+ const b = /* GraphQL */ `
49
+ query b {
50
+ me {
51
+ ... {
52
+ isLiked
53
+ }
54
+ ... @client {
55
+ localUser {
56
+ name
57
+ }
58
+ }
59
+ }
60
+ }
61
+ `;
62
+
63
+ const c = /* GraphQL */ `
64
+ query c {
65
+ me {
66
+ ...isLiked
67
+ }
68
+ }
69
+ fragment localUser on User @client {
70
+ localUser {
71
+ name
72
+ }
73
+ }
74
+ fragment isLiked on User {
75
+ isLiked
76
+ ...localUser
77
+ }
78
+ `;
79
+
80
+ const d = /* GraphQL */ `
81
+ fragment isLiked on User {
82
+ isLiked
83
+ }
84
+ query d {
85
+ me {
86
+ ...isLiked
87
+ ...locaUser
88
+ }
89
+ }
90
+ fragment localUser on User @client {
91
+ localUser {
92
+ name
93
+ }
94
+ }
95
+ `;
96
+
97
+ const e = /* GraphQL */ `
98
+ fragment friends on User {
99
+ friends {
100
+ ...isLiked
101
+ ... on User @client {
102
+ localUser {
103
+ name
104
+ }
105
+ }
106
+ }
107
+ }
108
+ query e {
109
+ isOnline @client
110
+ me {
111
+ ...friends
112
+ }
113
+ }
114
+ fragment isLiked on User {
115
+ isLiked
116
+ }
117
+ `;
118
+
119
+ // TODO support inline fragment spreads
120
+ const f = /* GraphQL */ `
121
+ query f {
122
+ me {
123
+ ...isLiked @client
124
+ }
125
+ }
126
+ fragment isLiked on User {
127
+ isLiked
128
+ }
129
+ `;
130
+
131
+ const rootURI = URI.file(process.cwd());
132
+
133
+ const config = parseApolloConfig({
134
+ client: {
135
+ service: {
136
+ name: "server",
137
+ localSchemaFile: "./schema.graphql",
138
+ },
139
+ includes: ["./src/**.graphql"],
140
+ excludes: ["./__tests__"],
141
+ validationRules: [NoMissingClientDirectives],
142
+ },
143
+ engine: {},
144
+ });
145
+
146
+ class MockLoadingHandler implements LoadingHandler {
147
+ handle<T>(_message: string, value: Promise<T>): Promise<T> {
148
+ return value;
149
+ }
150
+ handleSync<T>(_message: string, value: () => T): T {
151
+ return value();
152
+ }
153
+ showError(_message: string): void {}
154
+ }
155
+
156
+ jest.mock("fs");
157
+
158
+ describe("client state", () => {
159
+ beforeEach(() => {
160
+ vol.fromJSON({
161
+ "apollo.config.js": `module.exports = {
162
+ client: {
163
+ service: {
164
+ localSchemaFile: './schema.graphql'
165
+ }
166
+ }
167
+ }`,
168
+ "schema.graphql": serviceSchema,
169
+ "src/client-schema.graphql": clientSchema,
170
+ "src/a.graphql": a,
171
+ "src/b.graphql": b,
172
+ "src/c.graphql": c,
173
+ "src/d.graphql": d,
174
+ "src/e.graphql": e,
175
+ // "src/f.graphql": f,
176
+ });
177
+ });
178
+ afterEach(jest.restoreAllMocks);
179
+
180
+ it("should report validation errors for missing @client directives", async () => {
181
+ const project = new GraphQLClientProject({
182
+ config: config as ClientConfig,
183
+ loadingHandler: new MockLoadingHandler(),
184
+ configFolderURI: rootURI,
185
+ clientIdentity: {
186
+ name: "",
187
+ version: "",
188
+ referenceID: "",
189
+ },
190
+ });
191
+
192
+ const errors = Object.create(null);
193
+ project.onDiagnostics(({ diagnostics, uri }) => {
194
+ const path = basename(URI.parse(uri).path);
195
+ diagnostics.forEach(({ error }: any) => {
196
+ if (!errors[path]) errors[path] = [];
197
+ errors[path].push(error);
198
+ });
199
+ });
200
+
201
+ await project.whenReady;
202
+ await project.validate();
203
+
204
+ expect(errors).toMatchInlineSnapshot(`
205
+ Object {
206
+ "a.graphql": Array [
207
+ [GraphQLError: @client directive is missing on local field "isOnline"],
208
+ [GraphQLError: @client directive is missing on local field "isLiked"],
209
+ ],
210
+ "b.graphql": Array [
211
+ [GraphQLError: @client directive is missing on fragment around local fields "isLiked"],
212
+ ],
213
+ "c.graphql": Array [
214
+ [GraphQLError: @client directive is missing on fragment "isLiked" around local fields "isLiked,localUser"],
215
+ ],
216
+ "d.graphql": Array [
217
+ [GraphQLError: @client directive is missing on fragment "isLiked" around local fields "isLiked"],
218
+ ],
219
+ "e.graphql": Array [
220
+ [GraphQLError: @client directive is missing on fragment "isLiked" around local fields "isLiked"],
221
+ ],
222
+ }
223
+ `);
224
+ });
225
+ });