vscode-apollo 1.19.2 → 1.20.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 (155) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.circleci/config.yml +82 -0
  4. package/.eslintrc.js +10 -0
  5. package/.gitattributes +1 -0
  6. package/.github/workflows/release.yml +95 -0
  7. package/.gitleaks.toml +26 -0
  8. package/.nvmrc +1 -0
  9. package/.prettierrc +5 -0
  10. package/.vscode/launch.json +61 -0
  11. package/.vscode/settings.json +16 -0
  12. package/.vscode/tasks.json +18 -0
  13. package/.vscodeignore +17 -1
  14. package/CHANGELOG.md +172 -1
  15. package/LICENSE +2 -2
  16. package/README.md +9 -9
  17. package/codegen.yml +12 -0
  18. package/images/IconRun.svg +8 -0
  19. package/jest.config.ts +11 -0
  20. package/package.json +102 -22
  21. package/renovate.json +23 -0
  22. package/src/__mocks__/fs.js +3 -0
  23. package/src/__tests__/statusBar.test.ts +8 -7
  24. package/src/debug.ts +2 -5
  25. package/src/env/fetch/fetch.ts +32 -0
  26. package/src/env/fetch/global.ts +30 -0
  27. package/src/env/fetch/index.d.ts +2 -0
  28. package/src/env/fetch/index.ts +2 -0
  29. package/src/env/fetch/url.ts +9 -0
  30. package/src/env/index.ts +4 -0
  31. package/src/env/polyfills/array.ts +17 -0
  32. package/src/env/polyfills/index.ts +2 -0
  33. package/src/env/polyfills/object.ts +7 -0
  34. package/src/env/typescript-utility-types.ts +2 -0
  35. package/src/extension.ts +106 -37
  36. package/src/language-server/__tests__/diagnostics.test.ts +86 -0
  37. package/src/language-server/__tests__/document.test.ts +187 -0
  38. package/src/language-server/__tests__/fileSet.test.ts +46 -0
  39. package/src/language-server/__tests__/fixtures/starwarsSchema.ts +1917 -0
  40. package/src/language-server/config/__tests__/config.ts +128 -0
  41. package/src/language-server/config/__tests__/loadConfig.ts +508 -0
  42. package/src/language-server/config/__tests__/utils.ts +106 -0
  43. package/src/language-server/config/config.ts +219 -0
  44. package/src/language-server/config/index.ts +3 -0
  45. package/src/language-server/config/loadConfig.ts +228 -0
  46. package/src/language-server/config/utils.ts +56 -0
  47. package/src/language-server/diagnostics.ts +109 -0
  48. package/src/language-server/document.ts +277 -0
  49. package/src/language-server/engine/GraphQLDataSource.ts +124 -0
  50. package/src/language-server/engine/index.ts +105 -0
  51. package/src/language-server/engine/operations/frontendUrlRoot.ts +7 -0
  52. package/src/language-server/engine/operations/schemaTagsAndFieldStats.ts +24 -0
  53. package/src/language-server/errors/__tests__/NoMissingClientDirectives.test.ts +220 -0
  54. package/src/language-server/errors/logger.ts +58 -0
  55. package/src/language-server/errors/validation.ts +277 -0
  56. package/src/language-server/fileSet.ts +65 -0
  57. package/src/language-server/format.ts +48 -0
  58. package/src/language-server/graphqlTypes.ts +7176 -0
  59. package/src/language-server/index.ts +29 -0
  60. package/src/language-server/languageProvider.ts +798 -0
  61. package/src/language-server/loadingHandler.ts +64 -0
  62. package/src/language-server/project/base.ts +399 -0
  63. package/src/language-server/project/client.ts +602 -0
  64. package/src/language-server/project/defaultClientSchema.ts +45 -0
  65. package/src/language-server/project/service.ts +48 -0
  66. package/src/language-server/providers/schema/__tests__/file.ts +150 -0
  67. package/src/language-server/providers/schema/base.ts +15 -0
  68. package/src/language-server/providers/schema/endpoint.ts +157 -0
  69. package/src/language-server/providers/schema/engine.ts +197 -0
  70. package/src/language-server/providers/schema/file.ts +167 -0
  71. package/src/language-server/providers/schema/index.ts +75 -0
  72. package/src/language-server/server.ts +252 -0
  73. package/src/language-server/typings/codemirror.d.ts +4 -0
  74. package/src/language-server/typings/graphql.d.ts +27 -0
  75. package/src/language-server/utilities/__tests__/graphql.test.ts +411 -0
  76. package/src/language-server/utilities/__tests__/uri.ts +55 -0
  77. package/src/language-server/utilities/debouncer.ts +8 -0
  78. package/src/language-server/utilities/debug.ts +89 -0
  79. package/src/language-server/utilities/graphql.ts +432 -0
  80. package/src/language-server/utilities/index.ts +3 -0
  81. package/src/language-server/utilities/source.ts +182 -0
  82. package/src/language-server/utilities/uri.ts +19 -0
  83. package/src/language-server/workspace.ts +262 -0
  84. package/src/languageServerClient.ts +19 -12
  85. package/src/messages.ts +84 -0
  86. package/src/statusBar.ts +5 -5
  87. package/src/tools/__tests__/buildServiceDefinition.test.ts +491 -0
  88. package/src/tools/__tests__/snapshotSerializers/astSerializer.ts +19 -0
  89. package/src/tools/__tests__/snapshotSerializers/graphQLTypeSerializer.ts +14 -0
  90. package/src/tools/buildServiceDefinition.ts +241 -0
  91. package/src/tools/index.ts +6 -0
  92. package/src/tools/schema/index.ts +2 -0
  93. package/src/tools/schema/resolveObject.ts +18 -0
  94. package/src/tools/schema/resolverMap.ts +23 -0
  95. package/src/tools/utilities/graphql.ts +22 -0
  96. package/src/tools/utilities/index.ts +3 -0
  97. package/src/tools/utilities/invariant.ts +5 -0
  98. package/src/tools/utilities/predicates.ts +5 -0
  99. package/src/utils.ts +1 -16
  100. package/syntaxes/graphql.js.json +3 -3
  101. package/syntaxes/graphql.json +13 -9
  102. package/syntaxes/graphql.lua.json +51 -0
  103. package/syntaxes/graphql.rb.json +1 -1
  104. package/tsconfig.build.json +4 -0
  105. package/tsconfig.json +20 -7
  106. package/create-server-symlink.js +0 -8
  107. package/lib/debug.d.ts +0 -11
  108. package/lib/debug.d.ts.map +0 -1
  109. package/lib/debug.js +0 -48
  110. package/lib/debug.js.map +0 -1
  111. package/lib/extension.d.ts +0 -4
  112. package/lib/extension.d.ts.map +0 -1
  113. package/lib/extension.js +0 -187
  114. package/lib/extension.js.map +0 -1
  115. package/lib/languageServerClient.d.ts +0 -4
  116. package/lib/languageServerClient.d.ts.map +0 -1
  117. package/lib/languageServerClient.js +0 -57
  118. package/lib/languageServerClient.js.map +0 -1
  119. package/lib/statusBar.d.ts +0 -24
  120. package/lib/statusBar.d.ts.map +0 -1
  121. package/lib/statusBar.js +0 -46
  122. package/lib/statusBar.js.map +0 -1
  123. package/lib/testRunner/index.d.ts +0 -3
  124. package/lib/testRunner/index.d.ts.map +0 -1
  125. package/lib/testRunner/index.js +0 -49
  126. package/lib/testRunner/index.js.map +0 -1
  127. package/lib/testRunner/jest-config.d.ts +0 -14
  128. package/lib/testRunner/jest-config.d.ts.map +0 -1
  129. package/lib/testRunner/jest-config.js +0 -18
  130. package/lib/testRunner/jest-config.js.map +0 -1
  131. package/lib/testRunner/jest-vscode-environment.d.ts +0 -2
  132. package/lib/testRunner/jest-vscode-environment.d.ts.map +0 -1
  133. package/lib/testRunner/jest-vscode-environment.js +0 -19
  134. package/lib/testRunner/jest-vscode-environment.js.map +0 -1
  135. package/lib/testRunner/jest-vscode-framework-setup.d.ts +0 -1
  136. package/lib/testRunner/jest-vscode-framework-setup.d.ts.map +0 -1
  137. package/lib/testRunner/jest-vscode-framework-setup.js +0 -3
  138. package/lib/testRunner/jest-vscode-framework-setup.js.map +0 -1
  139. package/lib/testRunner/vscode-test-script.d.ts +0 -2
  140. package/lib/testRunner/vscode-test-script.d.ts.map +0 -1
  141. package/lib/testRunner/vscode-test-script.js +0 -23
  142. package/lib/testRunner/vscode-test-script.js.map +0 -1
  143. package/lib/utils.d.ts +0 -18
  144. package/lib/utils.d.ts.map +0 -1
  145. package/lib/utils.js +0 -52
  146. package/lib/utils.js.map +0 -1
  147. package/src/testRunner/README.md +0 -23
  148. package/src/testRunner/index.ts +0 -72
  149. package/src/testRunner/jest-config.ts +0 -17
  150. package/src/testRunner/jest-vscode-environment.ts +0 -25
  151. package/src/testRunner/jest-vscode-framework-setup.ts +0 -10
  152. package/src/testRunner/jest.d.ts +0 -37
  153. package/src/testRunner/vscode-test-script.ts +0 -38
  154. package/tsconfig.test.json +0 -4
  155. package/tsconfig.tsbuildinfo +0 -2486
@@ -0,0 +1,109 @@
1
+ import {
2
+ GraphQLSchema,
3
+ GraphQLError,
4
+ FragmentDefinitionNode,
5
+ findDeprecatedUsages,
6
+ isExecutableDefinitionNode,
7
+ } from "graphql";
8
+
9
+ import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver";
10
+
11
+ import { GraphQLDocument } from "./document";
12
+ import { highlightNodeForNode } from "./utilities/graphql";
13
+ import { rangeForASTNode } from "./utilities/source";
14
+
15
+ import { getValidationErrors } from "./errors/validation";
16
+ import { DocumentUri } from "./project/base";
17
+ import { ValidationRule } from "graphql/validation/ValidationContext";
18
+
19
+ /**
20
+ * Build an array of code diagnostics for all executable definitions in a document.
21
+ */
22
+ export function collectExecutableDefinitionDiagnositics(
23
+ schema: GraphQLSchema,
24
+ queryDocument: GraphQLDocument,
25
+ fragments: { [fragmentName: string]: FragmentDefinitionNode } = {},
26
+ rules?: ValidationRule[]
27
+ ): Diagnostic[] {
28
+ const ast = queryDocument.ast;
29
+ if (!ast) return queryDocument.syntaxErrors;
30
+
31
+ const astWithExecutableDefinitions = {
32
+ ...ast,
33
+ definitions: ast.definitions.filter(isExecutableDefinitionNode),
34
+ };
35
+
36
+ const diagnostics = [];
37
+
38
+ for (const error of getValidationErrors(
39
+ schema,
40
+ astWithExecutableDefinitions,
41
+ fragments,
42
+ rules
43
+ )) {
44
+ diagnostics.push(
45
+ ...diagnosticsFromError(error, DiagnosticSeverity.Error, "Validation")
46
+ );
47
+ }
48
+
49
+ for (const error of findDeprecatedUsages(
50
+ schema,
51
+ astWithExecutableDefinitions
52
+ )) {
53
+ diagnostics.push(
54
+ ...diagnosticsFromError(error, DiagnosticSeverity.Warning, "Deprecation")
55
+ );
56
+ }
57
+
58
+ return diagnostics;
59
+ }
60
+
61
+ export function diagnosticsFromError(
62
+ error: GraphQLError,
63
+ severity: DiagnosticSeverity,
64
+ type: string
65
+ ): GraphQLDiagnostic[] {
66
+ if (!error.nodes) {
67
+ return [];
68
+ }
69
+
70
+ return error.nodes.map((node) => {
71
+ return {
72
+ source: `GraphQL: ${type}`,
73
+ message: error.message,
74
+ severity,
75
+ range: rangeForASTNode(highlightNodeForNode(node) || node),
76
+ error,
77
+ };
78
+ });
79
+ }
80
+
81
+ export interface GraphQLDiagnostic extends Diagnostic {
82
+ /**
83
+ * The GraphQLError that produced this Diagnostic
84
+ */
85
+ error: GraphQLError;
86
+ }
87
+
88
+ export namespace GraphQLDiagnostic {
89
+ export function is(diagnostic: Diagnostic): diagnostic is GraphQLDiagnostic {
90
+ return "error" in diagnostic;
91
+ }
92
+ }
93
+
94
+ export class DiagnosticSet {
95
+ private diagnosticsByFile = new Map<DocumentUri, Diagnostic[]>();
96
+
97
+ entries() {
98
+ return this.diagnosticsByFile.entries();
99
+ }
100
+
101
+ addDiagnostics(uri: DocumentUri, diagnostics: Diagnostic[]) {
102
+ const existingDiagnostics = this.diagnosticsByFile.get(uri);
103
+ if (!existingDiagnostics) {
104
+ this.diagnosticsByFile.set(uri, diagnostics);
105
+ } else {
106
+ existingDiagnostics.push(...diagnostics);
107
+ }
108
+ }
109
+ }
@@ -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";
10
+
11
+ import { getRange as rangeOfTokenAtLocation } from "graphql-language-service-interface/dist/getDiagnostics";
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,124 @@
1
+ import { DataSourceConfig } from "apollo-datasource";
2
+ import { ApolloLink, execute, GraphQLRequest, makePromise } from "apollo-link";
3
+ import { setContext } from "apollo-link-context";
4
+ import { onError } from "apollo-link-error";
5
+ import { createHttpLink } from "apollo-link-http";
6
+ import {
7
+ ApolloError,
8
+ AuthenticationError,
9
+ ForbiddenError,
10
+ } from "apollo-server-errors";
11
+ import to from "await-to-js";
12
+ import { GraphQLError } from "graphql";
13
+ import { fetch } from "../../env";
14
+
15
+ export interface GraphQLResponse<T> {
16
+ data?: T;
17
+ errors?: GraphQLError[];
18
+ }
19
+ export class GraphQLDataSource<TContext = any> {
20
+ public baseURL!: string;
21
+ public context!: TContext;
22
+
23
+ public initialize(config: DataSourceConfig<TContext>): void {
24
+ this.context = config.context;
25
+ }
26
+
27
+ // XXX can we kill the casting here?
28
+ public async execute<T>(
29
+ operation: GraphQLRequest
30
+ ): Promise<GraphQLResponse<T>> {
31
+ return this.executeSingleOperation(operation) as Promise<
32
+ GraphQLResponse<T>
33
+ >;
34
+ }
35
+
36
+ protected willSendRequest?(request: any): any;
37
+
38
+ private composeLinks(): ApolloLink {
39
+ const uri = this.resolveUri();
40
+
41
+ return ApolloLink.from([
42
+ this.onErrorLink(),
43
+ this.onRequestLink(),
44
+ createHttpLink({ fetch, uri }),
45
+ ]);
46
+ }
47
+
48
+ private didEncounterError(error: any) {
49
+ const status = error.statusCode ? error.statusCode : null;
50
+ const message = error.bodyText
51
+ ? error.bodyText
52
+ : error.message
53
+ ? error.message
54
+ : null;
55
+
56
+ let apolloError: ApolloError;
57
+
58
+ switch (status) {
59
+ case 401:
60
+ apolloError = new AuthenticationError(message);
61
+ break;
62
+ case 403:
63
+ apolloError = new ForbiddenError(message);
64
+ break;
65
+ default:
66
+ apolloError = new ApolloError(message);
67
+ }
68
+
69
+ throw apolloError;
70
+ }
71
+
72
+ private async executeSingleOperation(operation: GraphQLRequest) {
73
+ const link = this.composeLinks();
74
+
75
+ const [error, response] = await to(makePromise(execute(link, operation)));
76
+
77
+ if (error) {
78
+ this.didEncounterError(error);
79
+ }
80
+
81
+ return response;
82
+ }
83
+
84
+ private resolveUri(): string {
85
+ const baseURL = this.baseURL;
86
+
87
+ if (!baseURL) {
88
+ throw new ApolloError(
89
+ "Cannot make request to GraphQL API, missing baseURL"
90
+ );
91
+ }
92
+
93
+ return baseURL;
94
+ }
95
+
96
+ private onRequestLink() {
97
+ return setContext((_, request) => {
98
+ if (this.willSendRequest) {
99
+ this.willSendRequest(request);
100
+ }
101
+
102
+ return request;
103
+ });
104
+ }
105
+
106
+ private onErrorLink() {
107
+ return onError(({ graphQLErrors, networkError, operation }) => {
108
+ const { result, response } = operation.getContext();
109
+ if (graphQLErrors) {
110
+ graphQLErrors.map((graphqlError) =>
111
+ console.error(`[GraphQL error]: ${graphqlError.message}`)
112
+ );
113
+ }
114
+
115
+ if (networkError) {
116
+ console.log(`[Network Error]: ${networkError}`);
117
+ }
118
+
119
+ if (response && response.status >= 400) {
120
+ console.log(`[Network Error] ${response.bodyText}`);
121
+ }
122
+ });
123
+ }
124
+ }
@@ -0,0 +1,105 @@
1
+ import { GraphQLDataSource } from "./GraphQLDataSource";
2
+ import { DefaultEngineConfig } from "../config";
3
+ import { SCHEMA_TAGS_AND_FIELD_STATS } from "./operations/schemaTagsAndFieldStats";
4
+ import {
5
+ FrontendUrlRootQuery,
6
+ SchemaTagsAndFieldStatsQuery,
7
+ } from "../graphqlTypes";
8
+ import { FRONTEND_URL_ROOT } from "./operations/frontendUrlRoot";
9
+
10
+ export interface ClientIdentity {
11
+ name?: string;
12
+ version?: string;
13
+ referenceID?: string;
14
+ }
15
+
16
+ export type ServiceID = string;
17
+ export type ClientID = string;
18
+ export type SchemaTag = string;
19
+ export type ServiceIDAndTag = [ServiceID, SchemaTag?];
20
+ export type ServiceSpecifier = string;
21
+ // Map from parent type name to field name to latency in ms.
22
+ export type FieldLatenciesMS = Map<string, Map<string, number | null>>;
23
+
24
+ export function noServiceError(service: string | undefined, endpoint?: string) {
25
+ return `Could not find graph ${
26
+ service ? service : ""
27
+ } from Apollo at ${endpoint}. Please check your API key and graph ID`;
28
+ }
29
+
30
+ export class ApolloEngineClient extends GraphQLDataSource {
31
+ constructor(
32
+ private engineKey: string,
33
+ engineEndpoint: string = DefaultEngineConfig.endpoint,
34
+ private clientIdentity?: ClientIdentity
35
+ ) {
36
+ super();
37
+ this.baseURL = engineEndpoint;
38
+ }
39
+
40
+ // XXX fix typings on base package
41
+ willSendRequest(request: any) {
42
+ if (!request.headers) request.headers = {};
43
+ request.headers["x-api-key"] = this.engineKey;
44
+ if (this.clientIdentity && this.clientIdentity.name) {
45
+ request.headers["apollo-client-name"] = this.clientIdentity.name;
46
+ request.headers["apollo-client-reference-id"] =
47
+ this.clientIdentity.referenceID;
48
+ request.headers["apollo-client-version"] = this.clientIdentity.version;
49
+ return;
50
+ }
51
+
52
+ // default values
53
+ request.headers["apollo-client-name"] = "Apollo Language Server";
54
+ request.headers["apollo-client-reference-id"] =
55
+ "146d29c0-912c-46d3-b686-920e52586be6";
56
+ request.headers["apollo-client-version"] =
57
+ require("../../package.json").version;
58
+ }
59
+
60
+ async loadSchemaTagsAndFieldLatencies(serviceID: string) {
61
+ const { data, errors } = await this.execute<SchemaTagsAndFieldStatsQuery>({
62
+ query: SCHEMA_TAGS_AND_FIELD_STATS,
63
+ variables: {
64
+ id: serviceID,
65
+ },
66
+ });
67
+
68
+ if (!(data && data.service && data.service.schemaTags) || errors) {
69
+ throw new Error(
70
+ errors
71
+ ? errors.map((error) => error.message).join("\n")
72
+ : "No service returned. Make sure your service name and API key match"
73
+ );
74
+ }
75
+
76
+ const schemaTags: string[] = data.service.schemaTags.map(
77
+ ({ tag }: { tag: string }) => tag
78
+ );
79
+
80
+ const fieldLatenciesMS: FieldLatenciesMS = new Map();
81
+
82
+ data.service.stats.fieldLatencies.forEach((fieldLatency) => {
83
+ const { parentType, fieldName } = fieldLatency.groupBy;
84
+
85
+ if (!parentType || !fieldName) {
86
+ return;
87
+ }
88
+ const fieldsMap =
89
+ fieldLatenciesMS.get(parentType) ||
90
+ fieldLatenciesMS.set(parentType, new Map()).get(parentType)!;
91
+
92
+ fieldsMap.set(fieldName, fieldLatency.metrics.fieldHistogram.durationMs);
93
+ });
94
+
95
+ return { schemaTags, fieldLatenciesMS };
96
+ }
97
+
98
+ async loadFrontendUrlRoot() {
99
+ const { data } = await this.execute<FrontendUrlRootQuery>({
100
+ query: FRONTEND_URL_ROOT,
101
+ });
102
+
103
+ return data?.frontendUrlRoot;
104
+ }
105
+ }
@@ -0,0 +1,7 @@
1
+ import gql from "graphql-tag";
2
+
3
+ export const FRONTEND_URL_ROOT = gql`
4
+ query FrontendUrlRoot {
5
+ frontendUrlRoot
6
+ }
7
+ `;
@@ -0,0 +1,24 @@
1
+ import gql from "graphql-tag";
2
+
3
+ export const SCHEMA_TAGS_AND_FIELD_STATS = gql`
4
+ query SchemaTagsAndFieldStats($id: ID!) {
5
+ service(id: $id) {
6
+ schemaTags {
7
+ tag
8
+ }
9
+ stats(from: "-86400", to: "-0") {
10
+ fieldLatencies {
11
+ groupBy {
12
+ parentType
13
+ fieldName
14
+ }
15
+ metrics {
16
+ fieldHistogram {
17
+ durationMs(percentile: 0.95)
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ `;