diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e7f2a0fa..37d4312b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - `apollo` - Use "table" package for tabular output and word wrap support [#1524](https://github.com/apollographql/apollo-tooling/pull/1524) - Use new single step mutation for checking federated service schemas [#1539](https://github.com/apollographql/apollo-tooling/pull/1539) + - Add support for `localSchemaFile` for federated service commands [#1489](https://github.com/apollographql/apollo-tooling/pull/1489) - `apollo-codegen-core` - - `apollo-codegen-flow` @@ -22,6 +23,8 @@ - - `apollo-language-server` - Replace old mutation used for checking partial service schemas to use `checkPartialSchema` [#1539](https://github.com/apollographql/apollo-tooling/pull/1539) + - Remove old federation-info provider [#1489](https://github.com/apollographql/apollo-tooling/pull/1489) + - Support using local schema files for checks/pushes of federated services [#1489](https://github.com/apollographql/apollo-tooling/pull/1489) - `apollo-tools` - - `vscode-apollo` diff --git a/package-lock.json b/package-lock.json index 6df9217198..68d61b7753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,6 +3,28 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "@apollo/federation": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.9.4.tgz", + "integrity": "sha512-s5vQ+yAJn+ULpWoiTS62iwa5VaI5wQ2zFSHUrhS1xurpKbkZA0gZ1P2XGJowAHKOd+mwLByKnwRxqkn0bQOGJQ==", + "requires": { + "apollo-env": "^0.5.1", + "apollo-graphql": "^0.3.3", + "apollo-server-env": "^2.4.3", + "lodash.xorby": "^4.7.0" + }, + "dependencies": { + "apollo-server-env": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.3.tgz", + "integrity": "sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + } + } + }, "@apollographql/apollo-tools": { "version": "file:packages/apollo-tools", "requires": { @@ -3606,6 +3628,7 @@ "apollo-language-server": { "version": "file:packages/apollo-language-server", "requires": { + "@apollo/federation": "0.9.4", "@apollographql/apollo-tools": "file:packages/apollo-tools", "@apollographql/graphql-language-service-interface": "^2.0.2", "@endemolshinegroup/cosmiconfig-typescript-loader": "^1.0.0", @@ -11818,6 +11841,11 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "lodash.xorby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.xorby/-/lodash.xorby-4.7.0.tgz", + "integrity": "sha1-nBmm+fBjputT3QPBtocXmYAUY9c=" + }, "log-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", diff --git a/packages/apollo-codegen-core/package.json b/packages/apollo-codegen-core/package.json index 8ddb3ab9d5..5dd9e1aa5f 100644 --- a/packages/apollo-codegen-core/package.json +++ b/packages/apollo-codegen-core/package.json @@ -1,7 +1,7 @@ { "name": "apollo-codegen-core", "description": "Core generator APIs for Apollo Codegen", - "version": "0.35.4-alpha.1", + "version": "0.36.0-alpha.0", "author": "Apollo GraphQL ", "license": "MIT", "repository": { diff --git a/packages/apollo-codegen-flow/package.json b/packages/apollo-codegen-flow/package.json index dc8c90abd9..3c78fc5e21 100644 --- a/packages/apollo-codegen-flow/package.json +++ b/packages/apollo-codegen-flow/package.json @@ -1,7 +1,7 @@ { "name": "apollo-codegen-flow", "description": "Flow generator module for Apollo Codegen", - "version": "0.33.29-alpha.1", + "version": "0.34.0-alpha.0", "author": "Apollo GraphQL ", "license": "MIT", "repository": { diff --git a/packages/apollo-codegen-scala/package.json b/packages/apollo-codegen-scala/package.json index df4f307170..6bae925e00 100644 --- a/packages/apollo-codegen-scala/package.json +++ b/packages/apollo-codegen-scala/package.json @@ -1,7 +1,7 @@ { "name": "apollo-codegen-scala", "description": "Scala generator module for Apollo Codegen", - "version": "0.34.29-alpha.1", + "version": "0.35.0-alpha.0", "author": "Apollo GraphQL ", "license": "MIT", "repository": { diff --git a/packages/apollo-codegen-swift/package.json b/packages/apollo-codegen-swift/package.json index 351f60844d..ad87680277 100644 --- a/packages/apollo-codegen-swift/package.json +++ b/packages/apollo-codegen-swift/package.json @@ -1,7 +1,7 @@ { "name": "apollo-codegen-swift", "description": "Swift generator module for Apollo Codegen", - "version": "0.35.9-alpha.1", + "version": "0.36.0-alpha.0", "author": "Apollo GraphQL ", "license": "MIT", "repository": { diff --git a/packages/apollo-codegen-typescript/package.json b/packages/apollo-codegen-typescript/package.json index bd46cb2981..b550d73517 100644 --- a/packages/apollo-codegen-typescript/package.json +++ b/packages/apollo-codegen-typescript/package.json @@ -1,7 +1,7 @@ { "name": "apollo-codegen-typescript", "description": "TypeScript generator module for Apollo Codegen", - "version": "0.35.4-alpha.1", + "version": "0.36.0-alpha.0", "author": "Apollo GraphQL ", "license": "MIT", "repository": { diff --git a/packages/apollo-language-server/package.json b/packages/apollo-language-server/package.json index 498b9aa105..f9c6411016 100644 --- a/packages/apollo-language-server/package.json +++ b/packages/apollo-language-server/package.json @@ -1,7 +1,7 @@ { "name": "apollo-language-server", "description": "A language server for Apollo GraphQL projects", - "version": "1.15.4-alpha.1", + "version": "1.16.0-alpha.0", "author": "Apollo GraphQL ", "license": "MIT", "repository": { @@ -17,6 +17,7 @@ "npm": ">=6" }, "dependencies": { + "@apollo/federation": "0.9.4", "@apollographql/apollo-tools": "file:../apollo-tools", "@apollographql/graphql-language-service-interface": "^2.0.2", "@endemolshinegroup/cosmiconfig-typescript-loader": "^1.0.0", diff --git a/packages/apollo-language-server/src/project/base.ts b/packages/apollo-language-server/src/project/base.ts index 0a3b9106b4..441e5d28a8 100644 --- a/packages/apollo-language-server/src/project/base.ts +++ b/packages/apollo-language-server/src/project/base.ts @@ -155,6 +155,10 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider { return this.schemaProvider.resolveSchema(config); } + public resolveFederatedServiceSDL() { + return this.schemaProvider.resolveFederatedServiceSDL(); + } + public onSchemaChange(handler: NotificationHandler) { this.lastLoadDate = +new Date(); return this.schemaProvider.onSchemaChange(handler); diff --git a/packages/apollo-language-server/src/project/service.ts b/packages/apollo-language-server/src/project/service.ts index 7e3fbdc316..19d65f353e 100644 --- a/packages/apollo-language-server/src/project/service.ts +++ b/packages/apollo-language-server/src/project/service.ts @@ -5,11 +5,6 @@ import { ServiceConfig } from "../config"; import { ClientIdentity } from "../engine"; import URI from "vscode-uri"; -import { - ApolloFederationInfoProvider, - federationInfoProviderFromConfig -} from "../providers/federation-info"; - export function isServiceProject( project: GraphQLProject ): project is GraphQLServiceProject { @@ -23,8 +18,6 @@ export interface GraphQLServiceProjectConfig { loadingHandler: LoadingHandler; } export class GraphQLServiceProject extends GraphQLProject { - public federationInfoProvider: ApolloFederationInfoProvider; - constructor({ clientIdentity, config, @@ -40,7 +33,6 @@ export class GraphQLServiceProject extends GraphQLProject { super({ config, fileSet, loadingHandler, clientIdentity }); this.config = config; - this.federationInfoProvider = federationInfoProviderFromConfig(config); } get displayName() { @@ -58,6 +50,6 @@ export class GraphQLServiceProject extends GraphQLProject { } resolveFederationInfo() { - return this.federationInfoProvider.resolveFederationInfo(); + return this.schemaProvider.resolveFederatedServiceSDL(); } } diff --git a/packages/apollo-language-server/src/providers/federation-info/base.ts b/packages/apollo-language-server/src/providers/federation-info/base.ts deleted file mode 100644 index 974684331b..0000000000 --- a/packages/apollo-language-server/src/providers/federation-info/base.ts +++ /dev/null @@ -1,12 +0,0 @@ -// These fields must exist for a service to be a proper -// federated service -export interface FederationInfo { - sdl: string; - name: string; - url: string; -} - -export interface ApolloFederationInfoProvider { - resolveFederationInfo(): Promise; - // TODO: onSchemaChange -} diff --git a/packages/apollo-language-server/src/providers/federation-info/endpoint.ts b/packages/apollo-language-server/src/providers/federation-info/endpoint.ts deleted file mode 100644 index a20550ef59..0000000000 --- a/packages/apollo-language-server/src/providers/federation-info/endpoint.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { execute as linkExecute, toPromise, from } from "apollo-link"; -import { createHttpLink, HttpLink } from "apollo-link-http"; -import { ExecutionResult, parse } from "graphql"; -import { Agent as HTTPSAgent } from "https"; -import { fetch } from "apollo-env"; -import { RemoteServiceConfig } from "../../config"; -import { ApolloFederationInfoProvider, FederationInfo } from "./base"; - -export class EndpointFederationInfoProvider - implements ApolloFederationInfoProvider { - private info?: FederationInfo; - - constructor(private config: Exclude) {} - - private async getFederationInfo() { - const { skipSSLValidation, url, headers } = this.config; - const options: HttpLink.Options = { - uri: url, - fetch - }; - if (url.startsWith("https:") && skipSSLValidation) { - options.fetchOptions = { - agent: new HTTPSAgent({ rejectUnauthorized: false }) - }; - } - - const getFederationInfoQuery = ` - query getFederationInfo { - _service { - sdl - } - } - `; - - const { data, errors } = (await toPromise( - linkExecute(createHttpLink(options), { - query: parse(getFederationInfoQuery), - context: { headers } - }) - )) as ExecutionResult<{ _service: FederationInfo }>; - - if (errors && errors.length) { - // XXX better error handling of GraphQL errors - throw new Error(errors.map(({ message }: Error) => message).join("\n")); - } - - if (!data || !data._service) { - throw new Error( - "No data received from server when querying for _service." - ); - } - - return data._service; - } - - async resolveFederationInfo() { - if (!this.info) { - this.info = await this.getFederationInfo(); - } - - if (!this.info) throw new Error("No service info returned by service"); - - return this.info; - } -} diff --git a/packages/apollo-language-server/src/providers/federation-info/index.ts b/packages/apollo-language-server/src/providers/federation-info/index.ts deleted file mode 100644 index 4eea950ce9..0000000000 --- a/packages/apollo-language-server/src/providers/federation-info/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApolloFederationInfoProvider } from "./base"; -import { - ApolloConfig, - isClientConfig, - isServiceConfig, - isLocalServiceConfig -} from "../../config"; - -import { EndpointFederationInfoProvider } from "./endpoint"; - -export { ApolloFederationInfoProvider }; - -export function federationInfoProviderFromConfig( - config: ApolloConfig -): ApolloFederationInfoProvider { - if (isServiceConfig(config)) { - // TODO: support files for federation info - if (config.service.endpoint) { - return new EndpointFederationInfoProvider(config.service.endpoint); - } - throw new Error( - "You must provide a service endpoint to use the federation info provider" - ); - } - - if (isClientConfig(config)) { - throw new Error( - "No federation info provider was created, because client projects are unsupported. For more information, please refer to https://bit.ly/2ByILPj" - ); - } - - throw new Error( - "No federation info provider was created, because the project type was unable to be resolved from your config. Please add a service config to use the SDL provider. For more information, please refer to https://bit.ly/2ByILPj" - ); -} diff --git a/packages/apollo-language-server/src/providers/schema/__tests__/file.ts b/packages/apollo-language-server/src/providers/schema/__tests__/file.ts new file mode 100644 index 0000000000..b953485ff8 --- /dev/null +++ b/packages/apollo-language-server/src/providers/schema/__tests__/file.ts @@ -0,0 +1,147 @@ +import { FileSchemaProvider } from "../file"; +import * as path from "path"; +import * as fs from "fs"; +import { Debug } from "../../../utilities"; +import { isDone } from "nock"; + +const makeNestedDir = dir => { + if (fs.existsSync(dir)) return; + + try { + fs.mkdirSync(dir); + } catch (err) { + if (err.code == "ENOENT") { + makeNestedDir(path.dirname(dir)); //create parent dir + makeNestedDir(dir); //create dir + } + } +}; + +const deleteFolderRecursive = path => { + // don't delete files on azure CI + if (process.env.AZURE_HTTP_USER_AGENT) return; + + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach(function(file, index) { + var curPath = path + "/" + file; + if (fs.lstatSync(curPath).isDirectory()) { + // recurse + deleteFolderRecursive(curPath); + } else { + // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } +}; + +const writeFilesToDir = (dir: string, files: Record) => { + Object.keys(files).forEach(key => { + if (key.includes("/")) makeNestedDir(path.dirname(key)); + fs.writeFileSync(`${dir}/${key}`, files[key]); + }); +}; + +describe("FileSchemaProvider", () => { + let dir, dirPath; + + // set up a temp dir + beforeEach(() => { + dir = fs.mkdtempSync("__tmp__"); + dirPath = `${process.cwd()}/${dir}`; + }); + + // clean up our temp dir + afterEach(() => { + if (dir) deleteFolderRecursive(dir); + dir = dirPath = undefined; + }); + + describe("resolveFederatedServiceSDL", () => { + it("finds and loads sdl from graphql file for a federated service", async () => { + writeFilesToDir(dir, { + "schema.graphql": ` + extend type Query { + myProduct: Product + } + + type Product @key(fields: "id") { + id: ID + sku: ID + name: String + } + ` + }); + + const provider = new FileSchemaProvider({ + path: dir + "/schema.graphql" + }); + const sdl = await provider.resolveFederatedServiceSDL(); + expect(sdl).toMatchInlineSnapshot; + }); + + it("finds and loads sdl from multiple graphql files for a federated service", async () => { + writeFilesToDir(dir, { + "schema.graphql": ` + extend type Query { + myProduct: Product + } + + type Product @key(fields: "id") { + id: ID + sku: ID + name: String + }`, + "schema2.graphql": ` + extend type Product { + weight: Float + }` + }); + + const provider = new FileSchemaProvider({ + paths: [dir + "/schema.graphql", dir + "/schema2.graphql"] + }); + const sdl = await provider.resolveFederatedServiceSDL(); + expect(sdl).toMatchInlineSnapshot(` + "type Product @key(fields: \\"id\\") { + id: ID + sku: ID + name: String + weight: Float + } + + extend type Query { + myProduct: Product + } + " + `); + }); + + it("errors when sdl file is not a graphql file", async () => { + const toWrite = ` + module.exports = \` + extend type Query { + myProduct: Product + } + + type Product @key(fields: "id") { + id: ID + sku: ID + name: string + }\` + `; + writeFilesToDir(dir, { + "schema.js": toWrite + }); + + // noop -- just spy on and silence the error + const errorSpy = jest.spyOn(Debug, "error"); + errorSpy.mockImplementation(() => {}); + + const provider = new FileSchemaProvider({ path: dir + "/schema.js" }); + const sdl = await provider.resolveFederatedServiceSDL(); + expect(errorSpy).toBeCalledTimes(2); + }); + }); +}); diff --git a/packages/apollo-language-server/src/providers/schema/base.ts b/packages/apollo-language-server/src/providers/schema/base.ts index 0c53241dfc..e0e3231048 100644 --- a/packages/apollo-language-server/src/providers/schema/base.ts +++ b/packages/apollo-language-server/src/providers/schema/base.ts @@ -11,4 +11,5 @@ export interface GraphQLSchemaProvider { onSchemaChange( handler: NotificationHandler ): SchemaChangeUnsubscribeHandler; + resolveFederatedServiceSDL(): Promise; } diff --git a/packages/apollo-language-server/src/providers/schema/introspection.ts b/packages/apollo-language-server/src/providers/schema/endpoint.ts similarity index 54% rename from packages/apollo-language-server/src/providers/schema/introspection.ts rename to packages/apollo-language-server/src/providers/schema/endpoint.ts index feaadf6c4a..252869afdd 100644 --- a/packages/apollo-language-server/src/providers/schema/introspection.ts +++ b/packages/apollo-language-server/src/providers/schema/endpoint.ts @@ -14,9 +14,12 @@ import { Agent as HTTPSAgent } from "https"; import { fetch } from "apollo-env"; import { RemoteServiceConfig } from "../../config"; import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base"; +import { Debug } from "../../utilities"; -export class IntrospectionSchemaProvider implements GraphQLSchemaProvider { +export class EndpointSchemaProvider implements GraphQLSchemaProvider { private schema?: GraphQLSchema; + private federatedServiceSDL?: string; + constructor(private config: Exclude) {} async resolveSchema() { if (this.schema) return this.schema; @@ -50,10 +53,61 @@ export class IntrospectionSchemaProvider implements GraphQLSchemaProvider { this.schema = buildClientSchema(data); return this.schema; } + onSchemaChange( _handler: NotificationHandler ): SchemaChangeUnsubscribeHandler { throw new Error("Polling of endpoint not implemented yet"); return () => {}; } + + async resolveFederatedServiceSDL() { + if (this.federatedServiceSDL) return this.federatedServiceSDL; + + const { skipSSLValidation, url, headers } = this.config; + const options: HttpLink.Options = { + uri: url, + fetch + }; + if (url.startsWith("https:") && skipSSLValidation) { + options.fetchOptions = { + agent: new HTTPSAgent({ rejectUnauthorized: false }) + }; + } + + const getFederationInfoQuery = ` + query getFederationInfo { + _service { + sdl + } + } + `; + + const { data, errors } = (await toPromise( + linkExecute(createHttpLink(options), { + query: parse(getFederationInfoQuery), + context: { headers } + }) + )) as ExecutionResult<{ _service: { sdl: string } }>; + + if (errors && errors.length) { + return Debug.error( + errors.map(({ message }: Error) => message).join("\n") + ); + } + + if (!data || !data._service) { + return Debug.error( + "No data received from server when querying for _service." + ); + } + + this.federatedServiceSDL = data._service.sdl; + return data._service.sdl; + } + + // public async isFederatedSchema() { + // const schema = this.schema || (await this.resolveSchema()); + // return false; + // } } diff --git a/packages/apollo-language-server/src/providers/schema/engine.ts b/packages/apollo-language-server/src/providers/schema/engine.ts index 5fb0a5da2a..29bfcd5aad 100644 --- a/packages/apollo-language-server/src/providers/schema/engine.ts +++ b/packages/apollo-language-server/src/providers/schema/engine.ts @@ -12,6 +12,7 @@ import { } from "./base"; import { GetSchemaByTag } from "../../graphqlTypes"; +import { Debug } from "../../utilities"; export class EngineSchemaProvider implements GraphQLSchemaProvider { private schema?: GraphQLSchema; @@ -84,6 +85,13 @@ export class EngineSchemaProvider implements GraphQLSchemaProvider { throw new Error("Polling of Engine not implemented yet"); return () => {}; } + + async resolveFederatedServiceSDL() { + Debug.error( + "Cannot resolve a federated service's SDL from engine. Use an endpoint or a file instead" + ); + return; + } } export const SCHEMA_QUERY = gql` diff --git a/packages/apollo-language-server/src/providers/schema/file.ts b/packages/apollo-language-server/src/providers/schema/file.ts index 47bf551c89..f216c565e1 100644 --- a/packages/apollo-language-server/src/providers/schema/file.ts +++ b/packages/apollo-language-server/src/providers/schema/file.ts @@ -1,88 +1,165 @@ -// FileSchemaProvider (FileProvider (SDL || IntrospectionResult) => schema) -import { - GraphQLSchema, - buildClientSchema, - Source, - buildSchema, - printSchema, - parse -} from "graphql"; -import { readFileSync } from "fs"; -import { extname, resolve } from "path"; -import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base"; -import { NotificationHandler } from "vscode-languageserver"; -import { buildSchemaFromSDL } from "apollo-graphql"; - -export interface FileSchemaProviderConfig { - path?: string; - paths?: string[]; -} - -export class FileSchemaProvider implements GraphQLSchemaProvider { - private schema?: GraphQLSchema; - - constructor(private config: FileSchemaProviderConfig) {} - - 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) - : undefined; - - if (!documents) - throw new Error( - `Schema could not be loaded for [${ - path ? path : paths ? paths.join(", ") : "undefined" - }]` - ); - - this.schema = buildSchemaFromSDL(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 - loadFileAndGetDocument(path: string) { - let result; - try { - result = readFileSync(path, { - encoding: "utf-8" - }); - } catch (err) { - throw new Error(`Unable to read file ${path}. ${err.message}`); - } - - const ext = extname(path); - - // an actual introspectionQuery 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") { - return parse(result); - } - 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 () => {}; - } -} +// FileSchemaProvider (FileProvider (SDL || IntrospectionResult) => schema) +import { + GraphQLSchema, + buildClientSchema, + Source, + buildSchema, + printSchema, + parse, + visit +} from "graphql"; +import { readFileSync } from "fs"; +import { extname, resolve } from "path"; +import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base"; +import { NotificationHandler } from "vscode-languageserver"; +import { Debug } from "../../utilities"; +import { buildSchemaFromSDL } from "apollo-graphql"; +import { + buildFederatedSchema, + composeServices, + printSchema as printFederatedSchema +} from "@apollo/federation"; +// 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) {} + + 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 = buildSchemaFromSDL(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 = readFileSync(path, { + encoding: "utf-8" + }); + } catch (err) { + throw new Error(`Unable to read file ${path}. ${err.message}`); + } + + const ext = extname(path); + + // an actual introspectionQuery 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") { + return parse(result); + } + 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 = buildFederatedSchema( + 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 && + serviceField.resolve && + serviceField.resolve(null, {}, null, {} as any); + + 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 = readFileSync(path, { + encoding: "utf-8" + }); + } catch (err) { + 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" + ); + } + } +} diff --git a/packages/apollo-language-server/src/providers/schema/index.ts b/packages/apollo-language-server/src/providers/schema/index.ts index 471d82bd43..46191aaaf4 100644 --- a/packages/apollo-language-server/src/providers/schema/index.ts +++ b/packages/apollo-language-server/src/providers/schema/index.ts @@ -10,7 +10,7 @@ import { isLocalServiceConfig } from "../../config"; -import { IntrospectionSchemaProvider } from "./introspection"; +import { EndpointSchemaProvider } from "./endpoint"; import { EngineSchemaProvider } from "./engine"; import { FileSchemaProvider } from "./file"; import { ClientIdentity } from "../../engine"; @@ -36,7 +36,7 @@ export function schemaProviderFromConfig( } if (config.service.endpoint) { - return new IntrospectionSchemaProvider(config.service.endpoint); + return new EndpointSchemaProvider(config.service.endpoint); } } @@ -59,7 +59,7 @@ export function schemaProviderFromConfig( ); } - return new IntrospectionSchemaProvider(config.client.service); + return new EndpointSchemaProvider(config.client.service); } } diff --git a/packages/apollo/README.md b/packages/apollo/README.md index 37f10a938b..d41a31e615 100644 --- a/packages/apollo/README.md +++ b/packages/apollo/README.md @@ -21,7 +21,7 @@ $ npm install -g apollo $ apollo COMMAND running command... $ apollo (-v|--version|version) -apollo/2.18.4-alpha.1 darwin-x64 node-v8.11.1 +apollo/2.19.0-alpha.0 darwin-x64 node-v8.11.1 $ apollo --help [COMMAND] USAGE $ apollo COMMAND diff --git a/packages/apollo/package.json b/packages/apollo/package.json index 100c7d93cd..9ff8150fdb 100644 --- a/packages/apollo/package.json +++ b/packages/apollo/package.json @@ -1,7 +1,7 @@ { "name": "apollo", "description": "Command line tool for Apollo GraphQL", - "version": "2.18.4-alpha.1", + "version": "2.19.0-alpha.0", "referenceID": "21ad0845-c235-422e-be7d-a998310df972", "author": "Apollo GraphQL ", "license": "MIT", diff --git a/packages/apollo/src/Command.ts b/packages/apollo/src/Command.ts index 573e75f561..5cd9c3d281 100644 --- a/packages/apollo/src/Command.ts +++ b/packages/apollo/src/Command.ts @@ -168,6 +168,7 @@ export abstract class ProjectCommand extends Command { }); } + // this can set a single or multiple local schema files if (flags.localSchemaFile) { const files = flags.localSchemaFile.split(","); if (isClientConfig(config)) { diff --git a/packages/apollo/src/commands/service/check.ts b/packages/apollo/src/commands/service/check.ts index 40d1ced5f1..12b44c9d9d 100644 --- a/packages/apollo/src/commands/service/check.ts +++ b/packages/apollo/src/commands/service/check.ts @@ -267,8 +267,7 @@ export default class ServiceCheck extends ProjectCommand { }), serviceName: flags.string({ description: - "Provides the name of the implementing service for a federated graph. This flag will indicate that the schema is a partial schema from a federated service", - dependsOn: ["endpoint"] + "Provides the name of the implementing service for a federated graph. This flag will indicate that the schema is a partial schema from a federated service" }) }; @@ -333,8 +332,8 @@ export default class ServiceCheck extends ProjectCommand { } task.output = "Fetching local service's partial schema"; - const info = await project.resolveFederationInfo(); - if (!info.sdl) { + const sdl = await project.resolveFederatedServiceSDL(); + if (!sdl) { throw new Error("No SDL found for federated service"); } @@ -357,9 +356,8 @@ export default class ServiceCheck extends ProjectCommand { graphVariant: tag, implementingServiceName: serviceName, partialSchema: { - sdl: info.sdl - }, - historicParameters + sdl + } }); task.title = `Found ${pluralize( diff --git a/packages/apollo/src/commands/service/push.ts b/packages/apollo/src/commands/service/push.ts index 17d20db9eb..d249ed0925 100644 --- a/packages/apollo/src/commands/service/push.ts +++ b/packages/apollo/src/commands/service/push.ts @@ -67,14 +67,14 @@ export default class ServicePush extends ProjectCommand { // handle partial schema uploading if (isFederated) { this.log("Fetching info from federated service"); - const info = await (project as GraphQLServiceProject).resolveFederationInfo(); + const sdl = await (project as GraphQLServiceProject).resolveFederatedServiceSDL(); - if (!info.sdl) + if (!sdl) throw new Error( "No SDL found in response from federated service. This means that the federated service exposed a `__service` field that did not emit errors, but that did not contain a spec-compliant `sdl` field." ); - if (!flags.serviceURL && !info.url) + if (!flags.serviceURL) throw new Error( "No URL found for federated service. Please provide the URL for the gateway to reach the service via the --serviceURL flag" ); @@ -94,19 +94,19 @@ export default class ServicePush extends ProjectCommand { } = await project.engine.uploadAndComposePartialSchema({ id: config.name, graphVariant: config.tag, - name: flags.serviceName || info.name, - url: flags.serviceURL || info.url, + name: flags.serviceName, + url: flags.serviceURL, revision: flags.serviceRevision || (gitContext && gitContext.commit) || "", activePartialSchema: { - sdl: info.sdl + sdl } }); result = { - implementingServiceName: flags.serviceName || info.name, + implementingServiceName: flags.serviceName, hash: compositionConfig && compositionConfig.schemaHash, compositionErrors: errors, serviceWasCreated, diff --git a/packages/vscode-apollo/package.json b/packages/vscode-apollo/package.json index 044ea8551d..b0bf04b522 100644 --- a/packages/vscode-apollo/package.json +++ b/packages/vscode-apollo/package.json @@ -2,7 +2,7 @@ "name": "vscode-apollo", "displayName": "Apollo GraphQL", "description": "Rich editor support for GraphQL client and server development that seamlessly integrates with the Apollo platform", - "version": "1.10.4-alpha.1", + "version": "1.11.0-alpha.0", "referenceID": "87197759-7617-40d0-b32e-46d378e907c7", "author": "Apollo GraphQL ", "license": "MIT",