// FileSchemaProvider (FileProvider (SDL || IntrospectionResult) => schema) import { GraphQLSchema, buildClientSchema, Source, printSchema, parse, buildASTSchema, } from "graphql"; import { mergeTypeDefs } from "@graphql-tools/merge"; import { existsSync, readFileSync } from "fs"; import { extname, resolve, isAbsolute } from "path"; import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base"; import { NotificationHandler } from "vscode-languageserver/node"; import { Debug } from "../../utilities"; import { buildSubgraphSchema } from "@apollo/subgraph"; import { URI } from "vscode-uri"; // import federationDirectives from "@apollo/federation/src/directives"; export interface FileSchemaProviderConfig { path?: string; paths?: string[]; } // XXX file subscription export class FileSchemaProvider implements GraphQLSchemaProvider { private schema?: GraphQLSchema; private federatedServiceSDL?: string; constructor( private config: FileSchemaProviderConfig, private configDir: URI | undefined, ) {} async resolveSchema() { if (this.schema) return this.schema; const { path, paths } = this.config; // load each path and get sdl string from each, if a list, concatenate them all const documents = path ? [this.loadFileAndGetDocument(path)] : paths ? paths.map(this.loadFileAndGetDocument, this) : undefined; if (!documents) throw new Error( `Schema could not be loaded for [${ path ? path : paths ? paths.join(", ") : "undefined" }]`, ); this.schema = buildASTSchema(mergeTypeDefs(documents)); if (!this.schema) throw new Error(`Schema could not be loaded for ${path}`); return this.schema; } // load a graphql file or introspection result and return the GraphQL DocumentNode // this is the mechanism for loading a single file's DocumentNode loadFileAndGetDocument(path: string) { let result; try { result = this.readFileSync(path); } catch (err: any) { throw new Error(`Unable to read file ${path}. ${err.message}`); } const ext = extname(path); // an actual introspection query result, convert to DocumentNode if (ext === ".json") { const parsed = JSON.parse(result); const __schema = parsed.data ? parsed.data.__schema : parsed.__schema ? parsed.__schema : parsed; const schema = buildClientSchema({ __schema }); return parse(printSchema(schema)); } else if (ext === ".graphql" || ext === ".graphqls" || ext === ".gql") { const uri = URI.file(resolve(path)).toString(); return parse(new Source(result, uri)); } throw new Error( "File Type not supported for schema loading. Must be a .json, .graphql, .gql, or .graphqls file", ); } onSchemaChange( _handler: NotificationHandler, ): SchemaChangeUnsubscribeHandler { throw new Error("File watching not implemented yet"); return () => {}; } // Load SDL from files. This is only used with federated services, // since they need full SDL and not the printout of GraphQLSchema async resolveFederatedServiceSDL() { if (this.federatedServiceSDL) return this.federatedServiceSDL; const { path, paths } = this.config; // load each path and get sdl string from each, if a list, concatenate them all const SDLs = path ? [this.loadFileAndGetSDL(path)] : paths ? paths.map(this.loadFileAndGetSDL, this) : undefined; if (!SDLs || SDLs.filter((s) => !Boolean(s)).length > 0) return Debug.error( `SDL could not be loaded for one of more files: [${ path ? path : paths ? paths.join(", ") : "undefined" }]`, ); const federatedSchema = buildSubgraphSchema( SDLs.map((sdl) => ({ typeDefs: parse(sdl as string) })), ); // call the `Query._service` resolver to get the actual printed sdl const queryType = federatedSchema.getQueryType(); if (!queryType) return Debug.error("No query type found for federated schema"); const serviceField = queryType.getFields()["_service"]; const serviceResults = serviceField?.resolve?.( null, {}, null, {} as any, ) as { sdl?: string }; if (!serviceResults || !serviceResults.sdl) return Debug.error( "No SDL resolver or result from federated schema after building", ); this.federatedServiceSDL = serviceResults.sdl; return this.federatedServiceSDL; } // this is the mechanism for loading a single file's SDL loadFileAndGetSDL(path: string) { let result; try { result = this.readFileSync(path); } catch (err: any) { return Debug.error(`Unable to read file ${path}. ${err.message}`); } const ext = extname(path); // this file should already be in sdl format if (ext === ".graphql" || ext === ".graphqls" || ext === ".gql") { return result as string; } else { return Debug.error( "When using localSchemaFile to check or push a federated service, you can only use .graphql, .gql, and .graphqls files", ); } } private readFileSync(path: string) { let finalPath = path; if (!isAbsolute(finalPath) && this.configDir) { const resolvedPath = resolve(this.configDir?.fsPath, path); if (existsSync(resolvedPath)) { finalPath = resolvedPath; } } return readFileSync(finalPath, { encoding: "utf-8", }); } }