1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.GraphQLClientProject = exports.isClientProject = void 0;
|
4 | const base_1 = require("./base");
|
5 | const graphql_1 = require("graphql");
|
6 | const vscode_languageserver_1 = require("vscode-languageserver");
|
7 | const source_1 = require("../utilities/source");
|
8 | const format_1 = require("../format");
|
9 | const fileSet_1 = require("../fileSet");
|
10 | const defaultClientSchema_1 = require("./defaultClientSchema");
|
11 | const graphql_2 = require("../utilities/graphql");
|
12 | const validation_1 = require("../errors/validation");
|
13 | const diagnostics_1 = require("../diagnostics");
|
14 | function schemaHasASTNodes(schema) {
|
15 | const queryType = schema && schema.getQueryType();
|
16 | return !!(queryType && queryType.astNode);
|
17 | }
|
18 | function augmentSchemaWithGeneratedSDLIfNeeded(schema) {
|
19 | if (schemaHasASTNodes(schema))
|
20 | return schema;
|
21 | const sdl = graphql_1.printSchema(schema);
|
22 | return graphql_1.buildSchema(new graphql_1.Source(sdl, `graphql-schema:/schema.graphql?${encodeURIComponent(sdl)}`));
|
23 | }
|
24 | function isClientProject(project) {
|
25 | return project instanceof GraphQLClientProject;
|
26 | }
|
27 | exports.isClientProject = isClientProject;
|
28 | class GraphQLClientProject extends base_1.GraphQLProject {
|
29 | constructor({ config, loadingHandler, rootURI, clientIdentity }) {
|
30 | const fileSet = new fileSet_1.FileSet({
|
31 | rootURI: config.configDirURI || rootURI,
|
32 | includes: [...config.client.includes, ".env", "apollo.config.js"],
|
33 | excludes: config.client.excludes,
|
34 | configURI: config.configURI
|
35 | });
|
36 | super({ config, fileSet, loadingHandler, clientIdentity });
|
37 | this.rootURI = rootURI;
|
38 | this.serviceID = config.graph;
|
39 | const filterConfigAndEnvFiles = (path) => !(path.includes("apollo.config") ||
|
40 | path.includes(".env") ||
|
41 | (config.configURI && path === config.configURI.fsPath));
|
42 | if (fileSet.allFiles().filter(filterConfigAndEnvFiles).length === 0) {
|
43 | console.warn("⚠️ It looks like there are 0 files associated with this Apollo Project. " +
|
44 | "This may be because you don't have any files yet, or your includes/excludes " +
|
45 | "fields are configured incorrectly, and Apollo can't find your files. " +
|
46 | "For help configuring Apollo projects, see this guide: https://go.apollo.dev/t/config");
|
47 | }
|
48 | const { validationRules } = this.config.client;
|
49 | if (typeof validationRules === "function") {
|
50 | this._validationRules = validation_1.defaultValidationRules.filter(validationRules);
|
51 | }
|
52 | else {
|
53 | this._validationRules = validationRules;
|
54 | }
|
55 | this.loadEngineData();
|
56 | }
|
57 | get displayName() {
|
58 | return this.config.graph || "Unnamed Project";
|
59 | }
|
60 | initialize() {
|
61 | return [this.scanAllIncludedFiles(), this.loadServiceSchema()];
|
62 | }
|
63 | getProjectStats() {
|
64 | const filterTypes = (type) => !/^__|Boolean|ID|Int|String|Float/.test(type);
|
65 | const serviceTypes = this.serviceSchema
|
66 | ? Object.keys(this.serviceSchema.getTypeMap()).filter(filterTypes).length
|
67 | : 0;
|
68 | const totalTypes = this.schema
|
69 | ? Object.keys(this.schema.getTypeMap()).filter(filterTypes).length
|
70 | : 0;
|
71 | return {
|
72 | type: "client",
|
73 | serviceId: this.serviceID,
|
74 | types: {
|
75 | service: serviceTypes,
|
76 | client: totalTypes - serviceTypes,
|
77 | total: totalTypes
|
78 | },
|
79 | tag: this.config.variant,
|
80 | loaded: Boolean(this.schema || this.serviceSchema),
|
81 | lastFetch: this.lastLoadDate
|
82 | };
|
83 | }
|
84 | onDecorations(handler) {
|
85 | this._onDecorations = handler;
|
86 | }
|
87 | onSchemaTags(handler) {
|
88 | this._onSchemaTags = handler;
|
89 | }
|
90 | async updateSchemaTag(tag) {
|
91 | await this.loadServiceSchema(tag);
|
92 | this.invalidate();
|
93 | }
|
94 | async loadServiceSchema(tag) {
|
95 | await this.loadingHandler.handle(`Loading schema for ${this.displayName}`, (async () => {
|
96 | this.serviceSchema = augmentSchemaWithGeneratedSDLIfNeeded(await this.schemaProvider.resolveSchema({
|
97 | tag: tag || this.config.variant,
|
98 | force: true
|
99 | }));
|
100 | this.schema = graphql_1.extendSchema(this.serviceSchema, this.clientSchema);
|
101 | })());
|
102 | }
|
103 | async resolveSchema() {
|
104 | if (!this.schema)
|
105 | throw new Error();
|
106 | return this.schema;
|
107 | }
|
108 | get clientSchema() {
|
109 | return {
|
110 | kind: graphql_1.Kind.DOCUMENT,
|
111 | definitions: [
|
112 | ...this.typeSystemDefinitionsAndExtensions,
|
113 | ...this.missingApolloClientDirectives
|
114 | ]
|
115 | };
|
116 | }
|
117 | get missingApolloClientDirectives() {
|
118 | const { serviceSchema } = this;
|
119 | const serviceDirectives = serviceSchema
|
120 | ? serviceSchema.getDirectives().map(directive => directive.name)
|
121 | : [];
|
122 | const clientDirectives = this.typeSystemDefinitionsAndExtensions
|
123 | .filter(graphql_2.isDirectiveDefinitionNode)
|
124 | .map(def => def.name.value);
|
125 | const existingDirectives = serviceDirectives.concat(clientDirectives);
|
126 | const apolloAst = defaultClientSchema_1.apolloClientSchemaDocument.ast;
|
127 | if (!apolloAst)
|
128 | return [];
|
129 | const apolloDirectives = apolloAst.definitions
|
130 | .filter(graphql_2.isDirectiveDefinitionNode)
|
131 | .map(def => def.name.value);
|
132 | for (const existingDirective of existingDirectives) {
|
133 | if (apolloDirectives.includes(existingDirective)) {
|
134 | return [];
|
135 | }
|
136 | }
|
137 | return apolloAst.definitions;
|
138 | }
|
139 | addClientMetadataToSchemaNodes() {
|
140 | const { schema, serviceSchema } = this;
|
141 | if (!schema || !serviceSchema)
|
142 | return;
|
143 | graphql_1.visit(this.clientSchema, {
|
144 | ObjectTypeExtension(node) {
|
145 | const type = schema.getType(node.name.value);
|
146 | const { fields } = node;
|
147 | if (!fields || !type)
|
148 | return;
|
149 | const localInfo = type.clientSchema || {};
|
150 | localInfo.localFields = [
|
151 | ...(localInfo.localFields || []),
|
152 | ...fields.map(field => field.name.value)
|
153 | ];
|
154 | type.clientSchema = localInfo;
|
155 | }
|
156 | });
|
157 | }
|
158 | async validate() {
|
159 | if (!this._onDiagnostics)
|
160 | return;
|
161 | if (!this.serviceSchema)
|
162 | return;
|
163 | const diagnosticSet = new diagnostics_1.DiagnosticSet();
|
164 | try {
|
165 | this.schema = graphql_1.extendSchema(this.serviceSchema, this.clientSchema);
|
166 | this.addClientMetadataToSchemaNodes();
|
167 | }
|
168 | catch (error) {
|
169 | if (error instanceof graphql_1.GraphQLError) {
|
170 | const uri = error.source && error.source.name;
|
171 | if (uri) {
|
172 | diagnosticSet.addDiagnostics(uri, diagnostics_1.diagnosticsFromError(error, vscode_languageserver_1.DiagnosticSeverity.Error, "Validation"));
|
173 | }
|
174 | }
|
175 | else {
|
176 | console.error(error);
|
177 | }
|
178 | this.schema = this.serviceSchema;
|
179 | }
|
180 | const fragments = this.fragments;
|
181 | for (const [uri, documentsForFile] of this.documentsByFile) {
|
182 | for (const document of documentsForFile) {
|
183 | diagnosticSet.addDiagnostics(uri, diagnostics_1.collectExecutableDefinitionDiagnositics(this.schema, document, fragments, this._validationRules));
|
184 | }
|
185 | }
|
186 | for (const [uri, diagnostics] of diagnosticSet.entries()) {
|
187 | this._onDiagnostics({ uri, diagnostics });
|
188 | }
|
189 | this.diagnosticSet = diagnosticSet;
|
190 | this.generateDecorations();
|
191 | }
|
192 | async loadEngineData() {
|
193 | const engineClient = this.engineClient;
|
194 | if (!engineClient)
|
195 | return;
|
196 | const serviceID = this.serviceID;
|
197 | if (!serviceID)
|
198 | return;
|
199 | await this.loadingHandler.handle(`Loading Apollo data for ${this.displayName}`, (async () => {
|
200 | try {
|
201 | const { schemaTags, fieldStats } = await engineClient.loadSchemaTagsAndFieldStats(serviceID);
|
202 | this._onSchemaTags && this._onSchemaTags([serviceID, schemaTags]);
|
203 | this.fieldStats = fieldStats;
|
204 | this.lastLoadDate = +new Date();
|
205 | this.generateDecorations();
|
206 | }
|
207 | catch (e) {
|
208 | console.error(e);
|
209 | }
|
210 | })());
|
211 | }
|
212 | generateDecorations() {
|
213 | if (!this._onDecorations)
|
214 | return;
|
215 | if (!this.schema)
|
216 | return;
|
217 | const decorations = [];
|
218 | for (const [uri, queryDocumentsForFile] of this.documentsByFile) {
|
219 | for (const queryDocument of queryDocumentsForFile) {
|
220 | if (queryDocument.ast && this.fieldStats) {
|
221 | const fieldStats = this.fieldStats;
|
222 | const typeInfo = new graphql_1.TypeInfo(this.schema);
|
223 | graphql_1.visit(queryDocument.ast, graphql_1.visitWithTypeInfo(typeInfo, {
|
224 | enter: node => {
|
225 | if (node.kind == "Field" && typeInfo.getParentType()) {
|
226 | const parentName = typeInfo.getParentType().name;
|
227 | const parentEngineStat = fieldStats.get(parentName);
|
228 | const engineStat = parentEngineStat
|
229 | ? parentEngineStat.get(node.name.value)
|
230 | : undefined;
|
231 | if (engineStat && engineStat > 1) {
|
232 | decorations.push({
|
233 | document: uri,
|
234 | message: `~${format_1.formatMS(engineStat, 0)}`,
|
235 | range: source_1.rangeForASTNode(node)
|
236 | });
|
237 | }
|
238 | }
|
239 | }
|
240 | }));
|
241 | }
|
242 | }
|
243 | }
|
244 | this._onDecorations(decorations);
|
245 | }
|
246 | get fragments() {
|
247 | const fragments = Object.create(null);
|
248 | for (const document of this.documents) {
|
249 | if (!document.ast)
|
250 | continue;
|
251 | for (const definition of document.ast.definitions) {
|
252 | if (definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) {
|
253 | fragments[definition.name.value] = definition;
|
254 | }
|
255 | }
|
256 | }
|
257 | return fragments;
|
258 | }
|
259 | get operations() {
|
260 | const operations = Object.create(null);
|
261 | for (const document of this.documents) {
|
262 | if (!document.ast)
|
263 | continue;
|
264 | for (const definition of document.ast.definitions) {
|
265 | if (definition.kind === graphql_1.Kind.OPERATION_DEFINITION) {
|
266 | if (!definition.name) {
|
267 | throw new graphql_1.GraphQLError("Apollo does not support anonymous operations", [definition]);
|
268 | }
|
269 | operations[definition.name.value] = definition;
|
270 | }
|
271 | }
|
272 | }
|
273 | return operations;
|
274 | }
|
275 | get mergedOperationsAndFragments() {
|
276 | return graphql_1.separateOperations({
|
277 | kind: graphql_1.Kind.DOCUMENT,
|
278 | definitions: [
|
279 | ...Object.values(this.fragments),
|
280 | ...Object.values(this.operations)
|
281 | ]
|
282 | });
|
283 | }
|
284 | get mergedOperationsAndFragmentsForService() {
|
285 | const { clientOnlyDirectives, clientSchemaDirectives, addTypename } = this.config.client;
|
286 | const current = this.mergedOperationsAndFragments;
|
287 | if ((!clientOnlyDirectives || !clientOnlyDirectives.length) &&
|
288 | (!clientSchemaDirectives || !clientSchemaDirectives.length))
|
289 | return current;
|
290 | const filtered = Object.create(null);
|
291 | for (const operationName in current) {
|
292 | const document = current[operationName];
|
293 | let serviceOnly = graphql_2.removeDirectiveAnnotatedFields(graphql_2.removeDirectives(document, clientOnlyDirectives), clientSchemaDirectives);
|
294 | if (addTypename)
|
295 | serviceOnly = graphql_2.withTypenameFieldAddedWhereNeeded(serviceOnly);
|
296 | if (serviceOnly.definitions.filter(Boolean).length) {
|
297 | filtered[operationName] = serviceOnly;
|
298 | }
|
299 | }
|
300 | return filtered;
|
301 | }
|
302 | getOperationFieldsFromFieldDefinition(fieldName, parent) {
|
303 | if (!this.schema || !parent)
|
304 | return [];
|
305 | const fields = [];
|
306 | const typeInfo = new graphql_1.TypeInfo(this.schema);
|
307 | for (const document of this.documents) {
|
308 | if (!document.ast)
|
309 | continue;
|
310 | graphql_1.visit(document.ast, graphql_1.visitWithTypeInfo(typeInfo, {
|
311 | Field(node) {
|
312 | if (node.name.value !== fieldName)
|
313 | return;
|
314 | const parentType = typeInfo.getParentType();
|
315 | if (parentType && parentType.name === parent.name.value) {
|
316 | fields.push(node);
|
317 | }
|
318 | return;
|
319 | }
|
320 | }));
|
321 | }
|
322 | return fields;
|
323 | }
|
324 | fragmentSpreadsForFragment(fragmentName) {
|
325 | const fragmentSpreads = [];
|
326 | for (const document of this.documents) {
|
327 | if (!document.ast)
|
328 | continue;
|
329 | graphql_1.visit(document.ast, {
|
330 | FragmentSpread(node) {
|
331 | if (node.name.value === fragmentName) {
|
332 | fragmentSpreads.push(node);
|
333 | }
|
334 | }
|
335 | });
|
336 | }
|
337 | return fragmentSpreads;
|
338 | }
|
339 | }
|
340 | exports.GraphQLClientProject = GraphQLClientProject;
|
341 |
|
\ | No newline at end of file |