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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.circleci/config.yml +91 -0
- package/.eslintrc.js +10 -0
- package/.git-blame-ignore-revs +2 -0
- package/.gitattributes +1 -0
- package/.github/workflows/release.yml +95 -0
- package/.gitleaks.toml +26 -0
- package/.nvmrc +1 -0
- package/.prettierrc +5 -0
- package/.vscode/launch.json +66 -0
- package/.vscode/settings.json +16 -0
- package/.vscode/tasks.json +60 -0
- package/.vscodeignore +28 -1
- package/CHANGELOG.md +250 -1
- package/CODEOWNERS +4 -0
- package/LICENSE +2 -2
- package/README.md +104 -55
- package/codegen.yml +12 -0
- package/graphql.configuration.json +5 -1
- package/images/IconRun.svg +8 -0
- package/images/marketplace/apollo-wordmark.png +0 -0
- package/jest.config.ts +21 -0
- package/jest.e2e.config.js +17 -0
- package/package.json +102 -23
- package/renovate.json +30 -0
- package/sampleWorkspace/clientSchema/apollo.config.cjs +10 -0
- package/sampleWorkspace/clientSchema/src/clientSchema.js +16 -0
- package/sampleWorkspace/clientSchema/src/test.js +18 -0
- package/sampleWorkspace/fixtures/starwarsSchema.graphql +299 -0
- package/sampleWorkspace/httpSchema/apollo.config.ts +8 -0
- package/sampleWorkspace/httpSchema/src/test.js +9 -0
- package/sampleWorkspace/localSchema/apollo.config.js +8 -0
- package/sampleWorkspace/localSchema/src/test.js +8 -0
- package/sampleWorkspace/localSchemaArray/apollo.config.js +12 -0
- package/sampleWorkspace/localSchemaArray/planets.graphql +20 -0
- package/sampleWorkspace/localSchemaArray/src/test.js +12 -0
- package/sampleWorkspace/sampleWorkspace.code-workspace +20 -0
- package/sampleWorkspace/spotifyGraph/apollo.config.mjs +5 -0
- package/sampleWorkspace/spotifyGraph/src/test.js +11 -0
- package/src/__e2e__/mockServer.js +117 -0
- package/src/__e2e__/mocks.js +13094 -0
- package/src/__e2e__/run.js +23 -0
- package/src/__e2e__/runTests.js +44 -0
- package/src/__e2e__/setup.js +1 -0
- package/src/__e2e__/vscode-environment.js +16 -0
- package/src/__e2e__/vscode.js +1 -0
- package/src/__mocks__/fs.js +3 -0
- package/src/__tests__/statusBar.test.ts +8 -7
- package/src/build.js +57 -0
- package/src/debug.ts +2 -5
- package/src/env/index.ts +1 -0
- package/src/env/typescript-utility-types.ts +2 -0
- package/src/extension.ts +265 -170
- package/src/language-server/__e2e__/clientSchema.e2e.ts +147 -0
- package/src/language-server/__e2e__/httpSchema.e2e.ts +21 -0
- package/src/language-server/__e2e__/localSchema.e2e.ts +25 -0
- package/src/language-server/__e2e__/localSchemaArray.e2e.ts +31 -0
- package/src/language-server/__e2e__/studioGraph.e2e.ts +65 -0
- package/src/language-server/__e2e__/utils.ts +151 -0
- package/src/language-server/__tests__/diagnostics.test.ts +86 -0
- package/src/language-server/__tests__/document.test.ts +187 -0
- package/src/language-server/__tests__/fileSet.test.ts +46 -0
- package/src/language-server/__tests__/fixtures/starwarsSchema.ts +1917 -0
- package/src/language-server/config/__tests__/config.ts +54 -0
- package/src/language-server/config/__tests__/loadConfig.ts +384 -0
- package/src/language-server/config/__tests__/utils.ts +99 -0
- package/src/language-server/config/config.ts +284 -0
- package/src/language-server/config/index.ts +3 -0
- package/src/language-server/config/loadConfig.ts +101 -0
- package/src/language-server/config/utils.ts +45 -0
- package/src/language-server/diagnostics.ts +118 -0
- package/src/language-server/document.ts +277 -0
- package/src/language-server/engine/index.ts +123 -0
- package/src/language-server/engine/operations/frontendUrlRoot.ts +15 -0
- package/src/language-server/engine/operations/schemaTagsAndFieldStats.ts +32 -0
- package/src/language-server/errors/__tests__/NoMissingClientDirectives.test.ts +225 -0
- package/src/language-server/errors/logger.ts +58 -0
- package/src/language-server/errors/validation.ts +274 -0
- package/src/language-server/fileSet.ts +63 -0
- package/src/language-server/format.ts +48 -0
- package/src/language-server/graphqlTypes.ts +16741 -0
- package/src/language-server/index.ts +28 -0
- package/src/language-server/languageProvider.ts +795 -0
- package/src/language-server/loadingHandler.ts +47 -0
- package/src/language-server/project/base.ts +406 -0
- package/src/language-server/project/client.ts +568 -0
- package/src/language-server/project/defaultClientSchema.ts +70 -0
- package/src/language-server/providers/schema/__tests__/file.ts +191 -0
- package/src/language-server/providers/schema/base.ts +15 -0
- package/src/language-server/providers/schema/endpoint.ts +138 -0
- package/src/language-server/providers/schema/engine.ts +204 -0
- package/src/language-server/providers/schema/file.ts +176 -0
- package/src/language-server/providers/schema/index.ts +59 -0
- package/src/language-server/server.ts +274 -0
- package/src/language-server/typings/graphql.d.ts +27 -0
- package/src/language-server/utilities/__tests__/graphql.test.ts +399 -0
- package/src/language-server/utilities/__tests__/uri.ts +55 -0
- package/src/language-server/utilities/debouncer.ts +8 -0
- package/src/language-server/utilities/debug.ts +90 -0
- package/src/language-server/utilities/graphql.ts +433 -0
- package/src/language-server/utilities/index.ts +3 -0
- package/src/language-server/utilities/source.ts +182 -0
- package/src/language-server/utilities/uri.ts +19 -0
- package/src/language-server/workspace.ts +254 -0
- package/src/languageServerClient.ts +22 -15
- package/src/messages.ts +75 -0
- package/src/statusBar.ts +5 -5
- package/src/tools/__tests__/buildServiceDefinition.test.ts +491 -0
- package/src/tools/__tests__/snapshotSerializers/astSerializer.ts +19 -0
- package/src/tools/__tests__/snapshotSerializers/graphQLTypeSerializer.ts +14 -0
- package/src/tools/buildServiceDefinition.ts +241 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/schema/index.ts +2 -0
- package/src/tools/schema/resolveObject.ts +18 -0
- package/src/tools/schema/resolverMap.ts +23 -0
- package/src/tools/utilities/graphql.ts +22 -0
- package/src/tools/utilities/index.ts +3 -0
- package/src/tools/utilities/invariant.ts +5 -0
- package/src/tools/utilities/predicates.ts +5 -0
- package/src/utils.ts +7 -21
- package/syntaxes/graphql.dart.json +2 -4
- package/syntaxes/graphql.ex.json +1 -4
- package/syntaxes/graphql.js.json +3 -3
- package/syntaxes/graphql.json +13 -9
- package/syntaxes/graphql.lua.json +51 -0
- package/syntaxes/graphql.rb.json +1 -1
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +22 -7
- package/create-server-symlink.js +0 -8
- package/lib/debug.d.ts +0 -11
- package/lib/debug.d.ts.map +0 -1
- package/lib/debug.js +0 -48
- package/lib/debug.js.map +0 -1
- package/lib/extension.d.ts +0 -4
- package/lib/extension.d.ts.map +0 -1
- package/lib/extension.js +0 -187
- package/lib/extension.js.map +0 -1
- package/lib/languageServerClient.d.ts +0 -4
- package/lib/languageServerClient.d.ts.map +0 -1
- package/lib/languageServerClient.js +0 -57
- package/lib/languageServerClient.js.map +0 -1
- package/lib/statusBar.d.ts +0 -24
- package/lib/statusBar.d.ts.map +0 -1
- package/lib/statusBar.js +0 -46
- package/lib/statusBar.js.map +0 -1
- package/lib/testRunner/index.d.ts +0 -3
- package/lib/testRunner/index.d.ts.map +0 -1
- package/lib/testRunner/index.js +0 -49
- package/lib/testRunner/index.js.map +0 -1
- package/lib/testRunner/jest-config.d.ts +0 -14
- package/lib/testRunner/jest-config.d.ts.map +0 -1
- package/lib/testRunner/jest-config.js +0 -18
- package/lib/testRunner/jest-config.js.map +0 -1
- package/lib/testRunner/jest-vscode-environment.d.ts +0 -2
- package/lib/testRunner/jest-vscode-environment.d.ts.map +0 -1
- package/lib/testRunner/jest-vscode-environment.js +0 -19
- package/lib/testRunner/jest-vscode-environment.js.map +0 -1
- package/lib/testRunner/jest-vscode-framework-setup.d.ts +0 -1
- package/lib/testRunner/jest-vscode-framework-setup.d.ts.map +0 -1
- package/lib/testRunner/jest-vscode-framework-setup.js +0 -3
- package/lib/testRunner/jest-vscode-framework-setup.js.map +0 -1
- package/lib/testRunner/vscode-test-script.d.ts +0 -2
- package/lib/testRunner/vscode-test-script.d.ts.map +0 -1
- package/lib/testRunner/vscode-test-script.js +0 -23
- package/lib/testRunner/vscode-test-script.js.map +0 -1
- package/lib/utils.d.ts +0 -18
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -52
- package/lib/utils.js.map +0 -1
- package/src/testRunner/README.md +0 -23
- package/src/testRunner/index.ts +0 -72
- package/src/testRunner/jest-config.ts +0 -17
- package/src/testRunner/jest-vscode-environment.ts +0 -25
- package/src/testRunner/jest-vscode-framework-setup.ts +0 -10
- package/src/testRunner/jest.d.ts +0 -37
- package/src/testRunner/vscode-test-script.ts +0 -38
- package/tsconfig.test.json +0 -4
- 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
|
+
});
|