From 47c86b6f4c34888bd9a81992d7064b56acd337b5 Mon Sep 17 00:00:00 2001 From: Johannes Schickling Date: Tue, 2 Jan 2018 23:40:20 +0100 Subject: [PATCH 1/7] Added subscriptions support to makeRemoteExectuableSchema --- .vscode/settings.json | 1 + package.json | 6 +- src/stitching/linkToFetcher.ts | 2 +- src/stitching/makeRemoteExecutableSchema.ts | 198 ++++++++++++-------- 4 files changed, 129 insertions(+), 78 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d988b1ebc0..e93660f7dbd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "editor.insertSpaces": true, "editor.rulers": [110], "editor.wordWrapColumn": 110, + "prettier.semi": true, "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "prettier.singleQuote": true, diff --git a/package.json b/package.json index 0de1ad33045..6e03e0b29e6 100644 --- a/package.json +++ b/package.json @@ -50,13 +50,14 @@ "dependencies": { "apollo-utilities": "^1.0.1", "deprecated-decorator": "^0.1.6", + "graphql-subscriptions": "^0.5.6", "uuid": "^3.1.0" }, "peerDependencies": { "graphql": "^0.11.0 || ^0.12.0" }, "devDependencies": { - "@types/chai": "4.0.4", + "@types/chai": "4.0.10", "@types/graphql": "0.11.7", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", @@ -70,13 +71,12 @@ "graphql-subscriptions": "^0.5.4", "graphql-type-json": "^0.1.4", "istanbul": "^0.4.5", - "iterall": "^1.1.3", "mocha": "^4.0.1", "prettier": "^1.7.4", "remap-istanbul": "0.9.5", "rimraf": "^2.6.2", "source-map-support": "^0.5.0", "tslint": "^5.8.0", - "typescript": "2.6.1" + "typescript": "2.6.2" } } diff --git a/src/stitching/linkToFetcher.ts b/src/stitching/linkToFetcher.ts index 18a4b1bc7d7..cd835c30f6b 100644 --- a/src/stitching/linkToFetcher.ts +++ b/src/stitching/linkToFetcher.ts @@ -45,7 +45,7 @@ function makePromise(observable: Observable): Promise { }); } -function execute( +export function execute( link: ApolloLink, operation: GraphQLRequest, ): Observable { diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 28ee534dbb4..b3fa1c5b9f7 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -1,9 +1,6 @@ -import { printSchema, Kind, ValueNode } from 'graphql'; -import linkToFetcher from './linkToFetcher'; - // This import doesn't actually import code - only the types. // Don't use ApolloLink to actually construct a link here. -import { ApolloLink } from 'apollo-link'; +import { ApolloLink } from 'apollo-link' import { GraphQLObjectType, @@ -20,78 +17,101 @@ import { ExecutionResult, print, buildSchema, -} from 'graphql'; -import isEmptyObject from '../isEmptyObject'; -import { IResolvers, IResolverObject } from '../Interfaces'; -import { makeExecutableSchema } from '../schemaGenerator'; -import resolveParentFromTypename from './resolveFromParentTypename'; -import defaultMergedResolver from './defaultMergedResolver'; -import { checkResultAndHandleErrors } from './errors'; - -export type Fetcher = (operation: FetcherOperation) => Promise; + printSchema, + Kind, + ValueNode, +} from 'graphql' +import linkToFetcher, { execute } from './linkToFetcher' +import isEmptyObject from '../isEmptyObject' +import { IResolvers, IResolverObject } from '../Interfaces' +import { makeExecutableSchema } from '../schemaGenerator' +import resolveParentFromTypename from './resolveFromParentTypename' +import defaultMergedResolver from './defaultMergedResolver' +import { checkResultAndHandleErrors } from './errors' +import { PubSub, ResolverFn } from 'graphql-subscriptions' + +export type Fetcher = (operation: FetcherOperation) => Promise export type FetcherOperation = { - query: string; - operationName?: string; - variables?: { [key: string]: any }; - context?: { [key: string]: any }; -}; + query: string + operationName?: string + variables?: { [key: string]: any } + context?: { [key: string]: any } +} export default function makeRemoteExecutableSchema({ schema, link, fetcher, }: { - schema: GraphQLSchema | string; - link?: ApolloLink; - fetcher?: Fetcher; + schema: GraphQLSchema | string + link?: ApolloLink + fetcher?: Fetcher }): GraphQLSchema { if (!fetcher && link) { - fetcher = linkToFetcher(link); + fetcher = linkToFetcher(link) } - let typeDefs: string; + let typeDefs: string if (typeof schema === 'string') { - typeDefs = schema; - schema = buildSchema(typeDefs); + typeDefs = schema + schema = buildSchema(typeDefs) } else { - typeDefs = printSchema(schema); + typeDefs = printSchema(schema) } - const queryType = schema.getQueryType(); - const queries = queryType.getFields(); - const queryResolvers: IResolverObject = {}; + // prepare query resolvers + const queryResolvers: IResolverObject = {} + const queryType = schema.getQueryType() + const queries = queryType.getFields() Object.keys(queries).forEach(key => { - queryResolvers[key] = createResolver(fetcher); - }); - let mutationResolvers: IResolverObject = {}; - const mutationType = schema.getMutationType(); + queryResolvers[key] = createResolver(fetcher) + }) + + // prepare mutation resolvers + const mutationResolvers: IResolverObject = {} + const mutationType = schema.getMutationType() if (mutationType) { - const mutations = mutationType.getFields(); + const mutations = mutationType.getFields() Object.keys(mutations).forEach(key => { - mutationResolvers[key] = createResolver(fetcher); - }); + mutationResolvers[key] = createResolver(fetcher) + }) + } + + // prepare subscription resolvers + const subscriptionResolvers: IResolverObject = {} + const subscriptionType = schema.getSubscriptionType() + if (subscriptionType) { + const subscriptions = subscriptionType.getFields() + Object.keys(subscriptions).forEach(key => { + subscriptionResolvers[key] = { + subscribe: createSubscriptionResolver(link), + } + }) } - const resolvers: IResolvers = { [queryType.name]: queryResolvers }; + // merge resolvers into resolver map + const resolvers: IResolvers = { [queryType.name]: queryResolvers } if (!isEmptyObject(mutationResolvers)) { - resolvers[mutationType.name] = mutationResolvers; + resolvers[mutationType.name] = mutationResolvers } - const typeMap = schema.getTypeMap(); - const types = Object.keys(typeMap).map(name => typeMap[name]); + if (!isEmptyObject(subscriptionResolvers)) { + resolvers[subscriptionType.name] = subscriptionResolvers + } + + // add missing abstract resolvers (scalar, unions, interfaces) + const typeMap = schema.getTypeMap() + const types = Object.keys(typeMap).map(name => typeMap[name]) for (const type of types) { - if ( - type instanceof GraphQLInterfaceType || - type instanceof GraphQLUnionType - ) { + if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { resolvers[type.name] = { __resolveType(parent, context, info) { - return resolveParentFromTypename(parent, info.schema); + return resolveParentFromTypename(parent, info.schema) }, - }; + } } else if (type instanceof GraphQLScalarType) { if ( !( @@ -102,90 +122,120 @@ export default function makeRemoteExecutableSchema({ type === GraphQLInt ) ) { - resolvers[type.name] = createPassThroughScalar(type); + resolvers[type.name] = createPassThroughScalar(type) } } else if ( type instanceof GraphQLObjectType && type.name.slice(0, 2) !== '__' && type !== queryType && - type !== mutationType + type !== mutationType && + type !== subscriptionType ) { - const resolver = {}; + const resolver = {} Object.keys(type.getFields()).forEach(field => { - resolver[field] = defaultMergedResolver; - }); - resolvers[type.name] = resolver; + resolver[field] = defaultMergedResolver + }) + resolvers[type.name] = resolver } } return makeExecutableSchema({ typeDefs, resolvers, - }); + }) } function createResolver(fetcher: Fetcher): GraphQLFieldResolver { return async (root, args, context, info) => { - const fragments = Object.keys(info.fragments).map( - fragment => info.fragments[fragment], - ); + const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]) const document = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments], - }; + } const result = await fetcher({ query: print(document), variables: info.variableValues, context: { graphqlContext: context }, - }); - return checkResultAndHandleErrors(result, info); - }; + }) + return checkResultAndHandleErrors(result, info) + } +} + +function createSubscriptionResolver(link: ApolloLink): ResolverFn { + return (root, args, context, info) => { + const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]) + const document = { + kind: Kind.DOCUMENT, + definitions: [info.operation, ...fragments], + } + + const operation = { + query: document, + variables: info.variableValues, + context: { graphqlContext: context }, + } + const observable = execute(link, operation) + + const pubSub = new PubSub() + const observer = { + next({ data }: any) { + pubSub.publish('static', data) + }, + error(err: Error) { + pubSub.publish('static', { errors: [err] }) + }, + } + + observable.subscribe(observer) + + return pubSub.asyncIterator('static') + } } function createPassThroughScalar({ name, description, }: { - name: string; - description: string; + name: string + description: string }): GraphQLScalarType { return new GraphQLScalarType({ name: name, description: description, serialize(value) { - return value; + return value }, parseValue(value) { - return value; + return value }, parseLiteral(ast) { - return parseLiteral(ast); + return parseLiteral(ast) }, - }); + }) } function parseLiteral(ast: ValueNode): any { switch (ast.kind) { case Kind.STRING: case Kind.BOOLEAN: { - return ast.value; + return ast.value } case Kind.INT: case Kind.FLOAT: { - return parseFloat(ast.value); + return parseFloat(ast.value) } case Kind.OBJECT: { - const value = Object.create(null); + const value = Object.create(null) ast.fields.forEach(field => { - value[field.name.value] = parseLiteral(field.value); - }); + value[field.name.value] = parseLiteral(field.value) + }) - return value; + return value } case Kind.LIST: { - return ast.values.map(parseLiteral); + return ast.values.map(parseLiteral) } default: - return null; + return null } } From 0d90d5a52b1d0ad9ea786ab69edd323b3ed835ee Mon Sep 17 00:00:00 2001 From: Johannes Schickling Date: Tue, 2 Jan 2018 23:42:11 +0100 Subject: [PATCH 2/7] Fix linting --- src/stitching/makeRemoteExecutableSchema.ts | 166 ++++++++++---------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index b3fa1c5b9f7..13f837b679c 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -1,6 +1,6 @@ // This import doesn't actually import code - only the types. // Don't use ApolloLink to actually construct a link here. -import { ApolloLink } from 'apollo-link' +import { ApolloLink } from 'apollo-link'; import { GraphQLObjectType, @@ -20,98 +20,98 @@ import { printSchema, Kind, ValueNode, -} from 'graphql' -import linkToFetcher, { execute } from './linkToFetcher' -import isEmptyObject from '../isEmptyObject' -import { IResolvers, IResolverObject } from '../Interfaces' -import { makeExecutableSchema } from '../schemaGenerator' -import resolveParentFromTypename from './resolveFromParentTypename' -import defaultMergedResolver from './defaultMergedResolver' -import { checkResultAndHandleErrors } from './errors' -import { PubSub, ResolverFn } from 'graphql-subscriptions' - -export type Fetcher = (operation: FetcherOperation) => Promise +} from 'graphql'; +import linkToFetcher, { execute } from './linkToFetcher'; +import isEmptyObject from '../isEmptyObject'; +import { IResolvers, IResolverObject } from '../Interfaces'; +import { makeExecutableSchema } from '../schemaGenerator'; +import resolveParentFromTypename from './resolveFromParentTypename'; +import defaultMergedResolver from './defaultMergedResolver'; +import { checkResultAndHandleErrors } from './errors'; +import { PubSub, ResolverFn } from 'graphql-subscriptions'; + +export type Fetcher = (operation: FetcherOperation) => Promise; export type FetcherOperation = { - query: string - operationName?: string - variables?: { [key: string]: any } - context?: { [key: string]: any } -} + query: string; + operationName?: string; + variables?: { [key: string]: any }; + context?: { [key: string]: any }; +}; export default function makeRemoteExecutableSchema({ schema, link, fetcher, }: { - schema: GraphQLSchema | string - link?: ApolloLink - fetcher?: Fetcher + schema: GraphQLSchema | string; + link?: ApolloLink; + fetcher?: Fetcher; }): GraphQLSchema { if (!fetcher && link) { - fetcher = linkToFetcher(link) + fetcher = linkToFetcher(link); } - let typeDefs: string + let typeDefs: string; if (typeof schema === 'string') { - typeDefs = schema - schema = buildSchema(typeDefs) + typeDefs = schema; + schema = buildSchema(typeDefs); } else { - typeDefs = printSchema(schema) + typeDefs = printSchema(schema); } // prepare query resolvers - const queryResolvers: IResolverObject = {} - const queryType = schema.getQueryType() - const queries = queryType.getFields() + const queryResolvers: IResolverObject = {}; + const queryType = schema.getQueryType(); + const queries = queryType.getFields(); Object.keys(queries).forEach(key => { - queryResolvers[key] = createResolver(fetcher) - }) + queryResolvers[key] = createResolver(fetcher); + }); // prepare mutation resolvers - const mutationResolvers: IResolverObject = {} - const mutationType = schema.getMutationType() + const mutationResolvers: IResolverObject = {}; + const mutationType = schema.getMutationType(); if (mutationType) { - const mutations = mutationType.getFields() + const mutations = mutationType.getFields(); Object.keys(mutations).forEach(key => { - mutationResolvers[key] = createResolver(fetcher) - }) + mutationResolvers[key] = createResolver(fetcher); + }); } // prepare subscription resolvers - const subscriptionResolvers: IResolverObject = {} - const subscriptionType = schema.getSubscriptionType() + const subscriptionResolvers: IResolverObject = {}; + const subscriptionType = schema.getSubscriptionType(); if (subscriptionType) { - const subscriptions = subscriptionType.getFields() + const subscriptions = subscriptionType.getFields(); Object.keys(subscriptions).forEach(key => { subscriptionResolvers[key] = { subscribe: createSubscriptionResolver(link), - } - }) + }; + }); } // merge resolvers into resolver map - const resolvers: IResolvers = { [queryType.name]: queryResolvers } + const resolvers: IResolvers = { [queryType.name]: queryResolvers }; if (!isEmptyObject(mutationResolvers)) { - resolvers[mutationType.name] = mutationResolvers + resolvers[mutationType.name] = mutationResolvers; } if (!isEmptyObject(subscriptionResolvers)) { - resolvers[subscriptionType.name] = subscriptionResolvers + resolvers[subscriptionType.name] = subscriptionResolvers; } // add missing abstract resolvers (scalar, unions, interfaces) - const typeMap = schema.getTypeMap() - const types = Object.keys(typeMap).map(name => typeMap[name]) + const typeMap = schema.getTypeMap(); + const types = Object.keys(typeMap).map(name => typeMap[name]); for (const type of types) { if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { resolvers[type.name] = { __resolveType(parent, context, info) { - return resolveParentFromTypename(parent, info.schema) + return resolveParentFromTypename(parent, info.schema); }, - } + }; } else if (type instanceof GraphQLScalarType) { if ( !( @@ -122,7 +122,7 @@ export default function makeRemoteExecutableSchema({ type === GraphQLInt ) ) { - resolvers[type.name] = createPassThroughScalar(type) + resolvers[type.name] = createPassThroughScalar(type); } } else if ( type instanceof GraphQLObjectType && @@ -131,111 +131,111 @@ export default function makeRemoteExecutableSchema({ type !== mutationType && type !== subscriptionType ) { - const resolver = {} + const resolver = {}; Object.keys(type.getFields()).forEach(field => { - resolver[field] = defaultMergedResolver - }) - resolvers[type.name] = resolver + resolver[field] = defaultMergedResolver; + }); + resolvers[type.name] = resolver; } } return makeExecutableSchema({ typeDefs, resolvers, - }) + }); } function createResolver(fetcher: Fetcher): GraphQLFieldResolver { return async (root, args, context, info) => { - const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]) + const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); const document = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments], - } + }; const result = await fetcher({ query: print(document), variables: info.variableValues, context: { graphqlContext: context }, - }) - return checkResultAndHandleErrors(result, info) - } + }); + return checkResultAndHandleErrors(result, info); + }; } function createSubscriptionResolver(link: ApolloLink): ResolverFn { return (root, args, context, info) => { - const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]) + const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); const document = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments], - } + }; const operation = { query: document, variables: info.variableValues, context: { graphqlContext: context }, - } - const observable = execute(link, operation) + }; + const observable = execute(link, operation); - const pubSub = new PubSub() + const pubSub = new PubSub(); const observer = { next({ data }: any) { - pubSub.publish('static', data) + pubSub.publish('static', data); }, error(err: Error) { - pubSub.publish('static', { errors: [err] }) + pubSub.publish('static', { errors: [err] }); }, - } + }; - observable.subscribe(observer) + observable.subscribe(observer); - return pubSub.asyncIterator('static') - } + return pubSub.asyncIterator('static'); + }; } function createPassThroughScalar({ name, description, }: { - name: string - description: string + name: string; + description: string; }): GraphQLScalarType { return new GraphQLScalarType({ name: name, description: description, serialize(value) { - return value + return value; }, parseValue(value) { - return value + return value; }, parseLiteral(ast) { - return parseLiteral(ast) + return parseLiteral(ast); }, - }) + }); } function parseLiteral(ast: ValueNode): any { switch (ast.kind) { case Kind.STRING: case Kind.BOOLEAN: { - return ast.value + return ast.value; } case Kind.INT: case Kind.FLOAT: { - return parseFloat(ast.value) + return parseFloat(ast.value); } case Kind.OBJECT: { - const value = Object.create(null) + const value = Object.create(null); ast.fields.forEach(field => { - value[field.name.value] = parseLiteral(field.value) - }) + value[field.name.value] = parseLiteral(field.value); + }); - return value + return value; } case Kind.LIST: { - return ast.values.map(parseLiteral) + return ast.values.map(parseLiteral); } default: - return null + return null; } } From 1a7d28607ef7412383780205eeea5f38dec4ac26 Mon Sep 17 00:00:00 2001 From: Johannes Schickling Date: Wed, 3 Jan 2018 11:25:07 +0100 Subject: [PATCH 3/7] Added optional PubSub parameter --- src/stitching/makeRemoteExecutableSchema.ts | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 13f837b679c..2eeb026634d 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -20,6 +20,7 @@ import { printSchema, Kind, ValueNode, + GraphQLResolveInfo, } from 'graphql'; import linkToFetcher, { execute } from './linkToFetcher'; import isEmptyObject from '../isEmptyObject'; @@ -28,7 +29,14 @@ import { makeExecutableSchema } from '../schemaGenerator'; import resolveParentFromTypename from './resolveFromParentTypename'; import defaultMergedResolver from './defaultMergedResolver'; import { checkResultAndHandleErrors } from './errors'; -import { PubSub, ResolverFn } from 'graphql-subscriptions'; +import { PubSub, PubSubEngine } from 'graphql-subscriptions'; + +export type ResolverFn = ( + rootValue?: any, + args?: any, + context?: any, + info?: GraphQLResolveInfo, +) => AsyncIterator; export type Fetcher = (operation: FetcherOperation) => Promise; @@ -43,10 +51,12 @@ export default function makeRemoteExecutableSchema({ schema, link, fetcher, + createPubSub, }: { schema: GraphQLSchema | string; link?: ApolloLink; fetcher?: Fetcher; + createPubSub?: () => PubSubEngine; }): GraphQLSchema { if (!fetcher && link) { fetcher = linkToFetcher(link); @@ -86,7 +96,7 @@ export default function makeRemoteExecutableSchema({ const subscriptions = subscriptionType.getFields(); Object.keys(subscriptions).forEach(key => { subscriptionResolvers[key] = { - subscribe: createSubscriptionResolver(link), + subscribe: createSubscriptionResolver(link, createPubSub), }; }); } @@ -161,7 +171,7 @@ function createResolver(fetcher: Fetcher): GraphQLFieldResolver { }; } -function createSubscriptionResolver(link: ApolloLink): ResolverFn { +function createSubscriptionResolver(link: ApolloLink, createPubSub?: () => PubSubEngine): ResolverFn { return (root, args, context, info) => { const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); const document = { @@ -176,10 +186,11 @@ function createSubscriptionResolver(link: ApolloLink): ResolverFn { }; const observable = execute(link, operation); - const pubSub = new PubSub(); + // fallback to in-memory PubSub if no PubSub provided + const pubSub = createPubSub ? createPubSub() : new PubSub(); const observer = { - next({ data }: any) { - pubSub.publish('static', data); + next(value: any) { + pubSub.publish('static', value.data); }, error(err: Error) { pubSub.publish('static', { errors: [err] }); From 138a726b981e87944b98f0164ca288c1edf479af Mon Sep 17 00:00:00 2001 From: Johannes Schickling Date: Wed, 3 Jan 2018 11:27:04 +0100 Subject: [PATCH 4/7] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cfb0a5e29..baf2604ecd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### vNEXT -* ... +* Added GraphQL Subscriptions support for schema stitching and `makeRemoteExecutableSchema` [PR #563](https://github.com/apollographql/graphql-tools/pull/563) ### v2.15.0 From 22d71500607a668de8fe41365f455919fcc2a3cc Mon Sep 17 00:00:00 2001 From: Johannes Schickling Date: Wed, 3 Jan 2018 11:32:25 +0100 Subject: [PATCH 5/7] Added iterall dev-dep to make tests work on Travis --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6e03e0b29e6..a6274cdabd6 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "graphql-subscriptions": "^0.5.4", "graphql-type-json": "^0.1.4", "istanbul": "^0.4.5", + "iterall": "^1.1.3", "mocha": "^4.0.1", "prettier": "^1.7.4", "remap-istanbul": "0.9.5", From 5e3dd1be0572b877943044b6eb6a22d1bcd2b4ca Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 3 Jan 2018 14:31:53 +0200 Subject: [PATCH 6/7] Tests --- src/stitching/makeRemoteExecutableSchema.ts | 18 +++++-- src/test/testMakeRemoteExecutableSchema.ts | 50 +++++++++++++++++++ src/test/testingSchemas.ts | 53 ++++++++++++++------- src/test/tests.ts | 1 + 4 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 src/test/testMakeRemoteExecutableSchema.ts diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 2eeb026634d..00d15ea035a 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -116,7 +116,10 @@ export default function makeRemoteExecutableSchema({ const typeMap = schema.getTypeMap(); const types = Object.keys(typeMap).map(name => typeMap[name]); for (const type of types) { - if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { + if ( + type instanceof GraphQLInterfaceType || + type instanceof GraphQLUnionType + ) { resolvers[type.name] = { __resolveType(parent, context, info) { return resolveParentFromTypename(parent, info.schema); @@ -157,7 +160,9 @@ export default function makeRemoteExecutableSchema({ function createResolver(fetcher: Fetcher): GraphQLFieldResolver { return async (root, args, context, info) => { - const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); + const fragments = Object.keys(info.fragments).map( + fragment => info.fragments[fragment], + ); const document = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments], @@ -171,9 +176,14 @@ function createResolver(fetcher: Fetcher): GraphQLFieldResolver { }; } -function createSubscriptionResolver(link: ApolloLink, createPubSub?: () => PubSubEngine): ResolverFn { +function createSubscriptionResolver( + link: ApolloLink, + createPubSub?: () => PubSubEngine, +): ResolverFn { return (root, args, context, info) => { - const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); + const fragments = Object.keys(info.fragments).map( + fragment => info.fragments[fragment], + ); const document = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments], diff --git a/src/test/testMakeRemoteExecutableSchema.ts b/src/test/testMakeRemoteExecutableSchema.ts new file mode 100644 index 00000000000..c920562f200 --- /dev/null +++ b/src/test/testMakeRemoteExecutableSchema.ts @@ -0,0 +1,50 @@ +/* tslint:disable:no-unused-expression */ + +import { expect } from 'chai'; +import { forAwaitEach } from 'iterall'; +import { GraphQLSchema, ExecutionResult, subscribe, parse } from 'graphql'; +import { + subscriptionSchema, + subscriptionPubSubTrigger, + subscriptionPubSub, + makeSchemaRemoteFromLink, +} from '../test/testingSchemas'; + +describe('remote subscriptions', () => { + let schema: GraphQLSchema; + before(async () => { + schema = await makeSchemaRemoteFromLink(subscriptionSchema); + }); + + it('should work', async done => { + const mockNotification = { + notifications: { + text: 'Hello world', + }, + }; + + const subscription = parse(` + subscription Subscription { + notifications { + text + } + } + `); + + let notificationCnt = 0; + subscribe(schema, subscription) + .then(results => { + forAwaitEach( + results as AsyncIterable, + (result: ExecutionResult) => { + expect(result).to.have.property('data'); + expect(result.data).to.deep.equal(mockNotification); + !notificationCnt++ ? done() : null; + }, + ).catch(done); + }) + .catch(done); + + subscriptionPubSub.publish(subscriptionPubSubTrigger, mockNotification); + }); +}); diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index 54d91663b77..2d850aa42f3 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -5,6 +5,7 @@ import { Kind, GraphQLScalarType, ValueNode, + ExecutionResult, } from 'graphql'; import { ApolloLink, Observable } from 'apollo-link'; import { makeExecutableSchema } from '../schemaGenerator'; @@ -560,24 +561,44 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ }); // Pretend this schema is remote -async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { +export async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { const link = new ApolloLink(operation => { return new Observable(observer => { - const { query, operationName, variables } = operation; - const { graphqlContext } = operation.getContext(); - graphql( - schema, - print(query), - null, - graphqlContext, - variables, - operationName, - ) - .then(result => { - observer.next(result); - observer.complete(); - }) - .catch(observer.error.bind(observer)); + (async () => { + const { query, operationName, variables } = operation; + const { graphqlContext } = operation.getContext(); + try { + const result: + | AsyncIterator + | ExecutionResult = await graphql( + schema, + print(query), + null, + graphqlContext, + variables, + operationName, + ); + if ( + typeof (>result).next === 'function' + ) { + while (true) { + const next = await (>result).next(); + observer.next(next.value); + if (next.done) { + observer.complete(); + break; + } + } + } else { + observer.next(result); + observer.complete(); + } + } catch (error) { + observer.error.bind(observer); + } + })(); }); }); diff --git a/src/test/tests.ts b/src/test/tests.ts index f12e88f0964..2ad0d870624 100755 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -4,4 +4,5 @@ import './testSchemaGenerator'; import './testLogger'; import './testMocking'; import './testResolution'; +import './testMakeRemoteExecutableSchema'; import './testMergeSchemas'; From f320d971589230ae1c592817896bbbc477163d9e Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 3 Jan 2018 15:04:05 +0200 Subject: [PATCH 7/7] Fixed tests, one pubsub for whole schema --- src/stitching/makeRemoteExecutableSchema.ts | 14 ++-- src/test/testMakeRemoteExecutableSchema.ts | 24 ++++--- src/test/testingSchemas.ts | 73 ++++++++++++++------- 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 00d15ea035a..ea7038b8506 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -93,10 +93,11 @@ export default function makeRemoteExecutableSchema({ const subscriptionResolvers: IResolverObject = {}; const subscriptionType = schema.getSubscriptionType(); if (subscriptionType) { + const pubSub = createPubSub ? createPubSub() : new PubSub(); const subscriptions = subscriptionType.getFields(); Object.keys(subscriptions).forEach(key => { subscriptionResolvers[key] = { - subscribe: createSubscriptionResolver(link, createPubSub), + subscribe: createSubscriptionResolver(key, link, pubSub), }; }); } @@ -177,8 +178,9 @@ function createResolver(fetcher: Fetcher): GraphQLFieldResolver { } function createSubscriptionResolver( + name: string, link: ApolloLink, - createPubSub?: () => PubSubEngine, + pubSub: PubSubEngine, ): ResolverFn { return (root, args, context, info) => { const fragments = Object.keys(info.fragments).map( @@ -196,20 +198,18 @@ function createSubscriptionResolver( }; const observable = execute(link, operation); - // fallback to in-memory PubSub if no PubSub provided - const pubSub = createPubSub ? createPubSub() : new PubSub(); const observer = { next(value: any) { - pubSub.publish('static', value.data); + pubSub.publish(`remote-schema-${name}`, value.data); }, error(err: Error) { - pubSub.publish('static', { errors: [err] }); + pubSub.publish(`remote-schema-${name}`, { errors: [err] }); }, }; observable.subscribe(observer); - return pubSub.asyncIterator('static'); + return pubSub.asyncIterator(`remote-schema-${name}`); }; } diff --git a/src/test/testMakeRemoteExecutableSchema.ts b/src/test/testMakeRemoteExecutableSchema.ts index c920562f200..7eb79dc6d05 100644 --- a/src/test/testMakeRemoteExecutableSchema.ts +++ b/src/test/testMakeRemoteExecutableSchema.ts @@ -16,7 +16,7 @@ describe('remote subscriptions', () => { schema = await makeSchemaRemoteFromLink(subscriptionSchema); }); - it('should work', async done => { + it('should work', done => { const mockNotification = { notifications: { text: 'Hello world', @@ -32,18 +32,16 @@ describe('remote subscriptions', () => { `); let notificationCnt = 0; - subscribe(schema, subscription) - .then(results => { - forAwaitEach( - results as AsyncIterable, - (result: ExecutionResult) => { - expect(result).to.have.property('data'); - expect(result.data).to.deep.equal(mockNotification); - !notificationCnt++ ? done() : null; - }, - ).catch(done); - }) - .catch(done); + subscribe(schema, subscription).then(results => + forAwaitEach( + results as AsyncIterable, + (result: ExecutionResult) => { + expect(result).to.have.property('data'); + expect(result.data).to.deep.equal(mockNotification); + !notificationCnt++ ? done() : null; + }, + ), + ); subscriptionPubSub.publish(subscriptionPubSubTrigger, mockNotification); }); diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index 2d850aa42f3..3ecbecc5633 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -2,6 +2,7 @@ import { GraphQLSchema, graphql, print, + subscribe, Kind, GraphQLScalarType, ValueNode, @@ -560,6 +561,18 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ resolvers: subscriptionResolvers, }); +const hasSubscriptionOperation = ({ query }: { query: any }): boolean => { + for (let definition of query.definitions) { + if (definition.kind === 'OperationDefinition') { + const operation = definition.operation; + if (operation === 'subscription') { + return true; + } + } + } + return false; +}; + // Pretend this schema is remote export async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { const link = new ApolloLink(operation => { @@ -568,32 +581,44 @@ export async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { const { query, operationName, variables } = operation; const { graphqlContext } = operation.getContext(); try { - const result: - | AsyncIterator - | ExecutionResult = await graphql( - schema, - print(query), - null, - graphqlContext, - variables, - operationName, - ); - if ( - typeof (>result).next === 'function' - ) { - while (true) { - const next = await (>result).next(); - observer.next(next.value); - if (next.done) { - observer.complete(); - break; - } - } - } else { + if (!hasSubscriptionOperation(operation)) { + const result = await graphql( + schema, + print(query), + null, + graphqlContext, + variables, + operationName, + ); observer.next(result); observer.complete(); + } else { + const result = await subscribe( + schema, + query, + null, + graphqlContext, + variables, + operationName, + ); + if ( + typeof (>result).next === + 'function' + ) { + while (true) { + const next = await (>result).next(); + observer.next(next.value); + if (next.done) { + observer.complete(); + break; + } + } + } else { + observer.next(result as ExecutionResult); + observer.complete(); + } } } catch (error) { observer.error.bind(observer);