From d70a8546e0ab1358014acfde0a29b4f2a4c4bf1e Mon Sep 17 00:00:00 2001 From: James Baxley Date: Tue, 3 Oct 2017 16:02:29 -0400 Subject: [PATCH 1/2] added native support for using apollo-link as the network layer --- CHANGELOG.md | 2 +- package.json | 1 + src/stitching/introspectSchema.ts | 16 ++++--- src/stitching/makeRemoteExecutableSchema.ts | 46 +++++++++++++++++---- src/test/testingSchemas.ts | 27 ++++++++++-- 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38256f1526b..78c9a456d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Change log ### vNEXT +* Added support for passing an Apollo Link instead of a fetcher ### v2.0.0 - * Add schema merging utilities [PR #382](https://github.com/apollographql/graphql-tools/pull/382) ### v1.2.3 diff --git a/package.json b/package.json index f83d0c5c531..5562c3e76b2 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "homepage": "https://github.com/apollostack/graphql-tools#readme", "dependencies": { + "apollo-link": "^0.7.0", "deprecated-decorator": "^0.1.6", "uuid": "^3.1.0" }, diff --git a/src/stitching/introspectSchema.ts b/src/stitching/introspectSchema.ts index b38228e61bd..633717eb0cc 100644 --- a/src/stitching/introspectSchema.ts +++ b/src/stitching/introspectSchema.ts @@ -1,15 +1,19 @@ -import { GraphQLSchema } from 'graphql'; +import { GraphQLSchema, parse } from 'graphql'; import { introspectionQuery, buildClientSchema } from 'graphql'; -import { Fetcher } from './makeRemoteExecutableSchema'; +import { ApolloLink, execute, makePromise } from 'apollo-link'; +import { Fetcher, fetcherToLink } from './makeRemoteExecutableSchema'; export default async function introspectSchema( - fetcher: Fetcher, + link: ApolloLink | Fetcher, context?: { [key: string]: any }, ): Promise { - const introspectionResult = await fetcher({ - query: introspectionQuery, + if (!(link as ApolloLink).request) { + link = fetcherToLink(link as Fetcher); + } + const introspectionResult = await makePromise(execute((link as ApolloLink), { + query: typeof introspectionQuery === 'string' ? parse(introspectionQuery) : introspectionQuery, context, - }); + })); if (introspectionResult.errors || !introspectionResult.data.__schema) { throw introspectionResult.errors; } else { diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 90198850012..67bc8655ccc 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -1,4 +1,6 @@ -import { printSchema, print, ExecutionResult, Kind, ValueNode } from 'graphql'; +import { printSchema, print, parse, Kind, ValueNode, ExecutionResult } from 'graphql'; +import { execute, makePromise, ApolloLink, Observable } from 'apollo-link'; + import { GraphQLFieldResolver, GraphQLSchema, @@ -25,25 +27,51 @@ export type Fetcher = ( }, ) => Promise; +export const fetcherToLink = (fetcher: Fetcher): ApolloLink => { + return new ApolloLink((operation) => { + return new Observable(observer => { + const { query, operationName, variables } = operation; + const context = operation.getContext(); + fetcher({ + query: typeof query === 'string' ? query : print(query), + operationName, + variables, + context + }) + .then((result: ExecutionResult) => { + observer.next(result); + observer.complete(); + }) + .catch(observer.error.bind(observer)); + }); + }); +}; + export default function makeRemoteExecutableSchema({ schema, - fetcher, + link, + fetcher }: { schema: GraphQLSchema; - fetcher: Fetcher; + link?: ApolloLink; + fetcher?: Fetcher; }): GraphQLSchema { + if (fetcher && !link) { + link = fetcherToLink(fetcher); + } + const queryType = schema.getQueryType(); const queries = queryType.getFields(); const queryResolvers: IResolverObject = {}; Object.keys(queries).forEach(key => { - queryResolvers[key] = createResolver(fetcher); + queryResolvers[key] = createResolver(link); }); let mutationResolvers: IResolverObject = {}; const mutationType = schema.getMutationType(); if (mutationType) { const mutations = mutationType.getFields(); Object.keys(mutations).forEach(key => { - mutationResolvers[key] = createResolver(fetcher); + mutationResolvers[key] = createResolver(link); }); } @@ -88,18 +116,18 @@ export default function makeRemoteExecutableSchema({ }); } -function createResolver(fetcher: Fetcher): GraphQLFieldResolver { +function createResolver(link: ApolloLink): GraphQLFieldResolver { return async (root, args, context, info) => { const operation = print(info.operation); const fragments = Object.keys(info.fragments) .map(fragment => print(info.fragments[fragment])) .join('\n'); const query = `${operation}\n${fragments}`; - const result = await fetcher({ - query, + const result = await makePromise(execute(link, { + query: typeof query === 'string' ? parse(query) : query, variables: info.variableValues, context, - }); + })); const fieldName = info.fieldNodes[0].alias ? info.fieldNodes[0].alias.value : info.fieldName; diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index 6712ebb2960..d2a1834499f 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -1,10 +1,12 @@ import { GraphQLSchema, graphql, + print, Kind, GraphQLScalarType, ValueNode, } from 'graphql'; +import { ApolloLink, Observable } from 'apollo-link'; import { makeExecutableSchema } from '../schemaGenerator'; import { IResolvers } from '../Interfaces'; import makeRemoteExecutableSchema from '../stitching/makeRemoteExecutableSchema'; @@ -485,7 +487,26 @@ export const bookingSchema: GraphQLSchema = makeExecutableSchema({ }); // Pretend this schema is remote -async function makeSchemaRemote(schema: GraphQLSchema) { +async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { + const link = new ApolloLink((operation) => { + return new Observable(observer => { + const { query, operationName, variables } = operation; + const context = operation.getContext(); + graphql(schema, print(query), null, context, variables, operationName) + .then((result) => { + observer.next(result); + observer.complete(); + }) + .catch(observer.error.bind(observer)); + }); + }); + + const clientSchema = await introspectSchema(link); + return makeRemoteExecutableSchema({ schema: clientSchema, link }); +} + +// ensure fetcher support exists from the 2.0 api +async function makeExecutableSchemaFromFetcher(schema: GraphQLSchema) { const fetcher: Fetcher = ({ query, operationName, variables, context }) => { return graphql(schema, query, null, context, variables, operationName); }; @@ -494,5 +515,5 @@ async function makeSchemaRemote(schema: GraphQLSchema) { return makeRemoteExecutableSchema({ schema: clientSchema, fetcher }); } -export const remotePropertySchema = makeSchemaRemote(propertySchema); -export const remoteBookingSchema = makeSchemaRemote(bookingSchema); +export const remotePropertySchema = makeSchemaRemoteFromLink(propertySchema); +export const remoteBookingSchema = makeExecutableSchemaFromFetcher(bookingSchema); From 67a0e0ab8aa72c05f84f751e93e6906d77e18589 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 4 Oct 2017 10:14:21 +0300 Subject: [PATCH 2/2] Minor optimizations to avoid excessive parsing/printing --- src/stitching/introspectSchema.ts | 12 +++++--- src/stitching/makeRemoteExecutableSchema.ts | 32 ++++++++++++--------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/stitching/introspectSchema.ts b/src/stitching/introspectSchema.ts index 633717eb0cc..9c664d24ddd 100644 --- a/src/stitching/introspectSchema.ts +++ b/src/stitching/introspectSchema.ts @@ -3,6 +3,8 @@ import { introspectionQuery, buildClientSchema } from 'graphql'; import { ApolloLink, execute, makePromise } from 'apollo-link'; import { Fetcher, fetcherToLink } from './makeRemoteExecutableSchema'; +const parsedIntrospectionQuery = parse(introspectionQuery); + export default async function introspectSchema( link: ApolloLink | Fetcher, context?: { [key: string]: any }, @@ -10,10 +12,12 @@ export default async function introspectSchema( if (!(link as ApolloLink).request) { link = fetcherToLink(link as Fetcher); } - const introspectionResult = await makePromise(execute((link as ApolloLink), { - query: typeof introspectionQuery === 'string' ? parse(introspectionQuery) : introspectionQuery, - context, - })); + const introspectionResult = await makePromise( + execute(link as ApolloLink, { + query: parsedIntrospectionQuery, + context, + }), + ); if (introspectionResult.errors || !introspectionResult.data.__schema) { throw introspectionResult.errors; } else { diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 67bc8655ccc..949251519c8 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -1,4 +1,4 @@ -import { printSchema, print, parse, Kind, ValueNode, ExecutionResult } from 'graphql'; +import { printSchema, print, Kind, ValueNode, ExecutionResult } from 'graphql'; import { execute, makePromise, ApolloLink, Observable } from 'apollo-link'; import { @@ -28,7 +28,7 @@ export type Fetcher = ( ) => Promise; export const fetcherToLink = (fetcher: Fetcher): ApolloLink => { - return new ApolloLink((operation) => { + return new ApolloLink(operation => { return new Observable(observer => { const { query, operationName, variables } = operation; const context = operation.getContext(); @@ -36,7 +36,7 @@ export const fetcherToLink = (fetcher: Fetcher): ApolloLink => { query: typeof query === 'string' ? query : print(query), operationName, variables, - context + context, }) .then((result: ExecutionResult) => { observer.next(result); @@ -50,7 +50,7 @@ export const fetcherToLink = (fetcher: Fetcher): ApolloLink => { export default function makeRemoteExecutableSchema({ schema, link, - fetcher + fetcher, }: { schema: GraphQLSchema; link?: ApolloLink; @@ -118,16 +118,20 @@ export default function makeRemoteExecutableSchema({ function createResolver(link: ApolloLink): GraphQLFieldResolver { return async (root, args, context, info) => { - const operation = print(info.operation); - const fragments = Object.keys(info.fragments) - .map(fragment => print(info.fragments[fragment])) - .join('\n'); - const query = `${operation}\n${fragments}`; - const result = await makePromise(execute(link, { - query: typeof query === 'string' ? parse(query) : query, - variables: info.variableValues, - context, - })); + const fragments = Object.keys(info.fragments).map( + fragment => info.fragments[fragment], + ); + const document = { + kind: Kind.DOCUMENT, + definitions: [info.operation, ...fragments], + }; + const result = await makePromise( + execute(link, { + query: document, + variables: info.variableValues, + context, + }), + ); const fieldName = info.fieldNodes[0].alias ? info.fieldNodes[0].alias.value : info.fieldName;