diff --git a/docs/source/directive-resolvers.md b/docs/source/directive-resolvers.md index 1e7e7c1faf8..9c4e988398f 100644 --- a/docs/source/directive-resolvers.md +++ b/docs/source/directive-resolvers.md @@ -2,7 +2,6 @@ ## Directive example Let's take a look at how we can create `@upper` Directive to upper-case a string returned from resolve on Field -[See a complete runnable example on Launchpad.](https://launchpad.graphql.com/p00rw37qx0) To start, let's grab the schema definition string from the `makeExecutableSchema` example [in the "Generating a schema" article](/generate-schema/#example). @@ -65,7 +64,6 @@ graphql(schema, query).then((result) => console.log('Got result', result)); ## Multi-Directives example Multi-Directives on a field will be apply with LTR order. -[See a complete runnable example on Launchpad.](https://launchpad.graphql.com/nx945rq1x7) ```js // graphql-tools combines a schema string with resolvers. diff --git a/docs/source/generate-schema.md b/docs/source/generate-schema.md index f95b952d5f7..be469554e59 100644 --- a/docs/source/generate-schema.md +++ b/docs/source/generate-schema.md @@ -7,8 +7,6 @@ The graphql-tools package allows you to create a GraphQL.js GraphQLSchema instan ## Example -[See the complete live example in Apollo Launchpad.](https://launchpad.graphql.com/1jzxrj179) - When using `graphql-tools`, you describe the schema as a GraphQL type language string: ```js diff --git a/docs/source/schema-stitching.md b/docs/source/schema-stitching.md index 19413e9ed4e..4ee09ced363 100644 --- a/docs/source/schema-stitching.md +++ b/docs/source/schema-stitching.md @@ -7,19 +7,13 @@ description: Combining multiple GraphQL APIs into one Schema stitching is the process of creating a single GraphQL schema from multiple underlying GraphQL APIs. -One of the main benefits of GraphQL is that we can query all of our data as part of one schema, and get everything we need in one request. But as the schema grows, it might become cumbersome to manage it all as one codebase, and it starts to make sense to split it into different modules. We may also want to decompose your schema into separate microservices, which can be developed and deployed independently. +One of the main benefits of GraphQL is that we can query all of our data as part of one schema, and get everything we need in one request. But as the schema grows, it might become cumbersome to manage it all as one codebase, and it starts to make sense to split it into different modules. We may also want to decompose your schema into separate microservices, which can be developed and deployed independently. We may also want to integrate our own schema with remote schemas. -In both cases, we use `mergeSchemas` to combine multiple GraphQL schemas together and produce a merged schema that knows how to delegate parts of the query to the relevant subschemas. These subschemas can be either local to the server, or running on a remote server. They can even be services offered by 3rd parties, allowing us to connect to external data and create mashups. - -## Working with remote schemas - -In order to merge with a remote schema, we first call [makeRemoteExecutableSchema](/remote-schemas/) to create a local proxy for the schema that knows how to call the remote endpoint. We then merge that local proxy schema the same way we would merge any other locally implemented schema. +In these cases, we use `mergeSchemas` to combine multiple GraphQL schemas together and produce a new schema that knows how to delegate parts of the query to the relevant subschemas. These subschemas can be either local to the server, or running on a remote server. They can even be services offered by 3rd parties, allowing us to connect to external data and create mashups. ## Basic example -In this example we'll stitch together two very simple schemas. It doesn't matter whether these are local or proxies created with `makeRemoteExecutableSchema`, because the merging itself would be the same. - -In this case, we're dealing with two schemas that implement a system with users and "chirps"—small snippets of text that users can post. +In this example we'll stitch together two very simple schemas. In this case, we're dealing with two schemas that implement a system with users and "chirps"—small snippets of text that users can post. ```js import { @@ -65,14 +59,16 @@ const authorSchema = makeExecutableSchema({ addMockFunctionsToSchema({ schema: authorSchema }); export const schema = mergeSchemas({ - schemas: [ - chirpSchema, - authorSchema, + subschemas: [ + { schema: chirpSchema, }, + { schema: authorSchema, }, ], }); ``` -[Run the above example on Launchpad.](https://launchpad.graphql.com/1nkk8vqj9) +Note the new `subschemas` property with an array of subschema configuration objects. This syntax is a bit more verbose, but we shall see how it provides multiple benefits: +1. transforms can be specified on the subschema config object, avoiding creation of a new schema with a new round of delegation in order to transform a schema prior to merging. +2. remote schema configuration options can be specified, also avoiding an additional round of schema proxying. This gives us a new schema with the root fields on `Query` from both schemas (along with the `User` and `Chirp` types): @@ -107,32 +103,34 @@ const linkTypeDefs = ` We can now merge these three schemas together: ```js -mergeSchemas({ - schemas: [ - chirpSchema, - authorSchema, - linkTypeDefs, +export const schema = mergeSchemas({ + subschemas: [ + { schema: chirpSchema, }, + { schema: authorSchema, }, ], + typeDefs: linkTypeDefs, }); ``` +Note the new `typeDefs` option in parallel to the new `subschemas` option, which better expresses that these typeDefs are defined only within the outer gateway schemas. + We won't be able to query `User.chirps` or `Chirp.author` yet, however, because we still need to define resolvers for these new fields. How should these resolvers be implemented? When we resolve `User.chirps` or `Chirp.author`, we want to _delegate_ to the relevant root fields. To get from a user to the user's chirps, for example, we'll want to use the `id` of the user to call `Query.chirpsByAuthorId`. And to get from a chirp to its author, we can use the chirp's `authorId` field to call the existing `Query.userById` field. -Resolvers for fields in schemas created by `mergeSchema` can use the `delegateToSchema` function to forward parts of queries (or even whole new queries) to one of the subschemas that was passed to `mergeSchemas` (or any other schema). +Resolvers can use the `delegateToSchema` function to forward parts of queries (or even whole new queries) to one of the subschemas that was passed to `mergeSchemas` (or any other schema). In order to delegate to these root fields, we'll need to make sure we've actually requested the `id` of the user or the `authorId` of the chirp. To avoid forcing users to add these fields to their queries manually, resolvers on a merged schema can define a `fragment` property that specifies the required fields, and they will be added to the query automatically. A complete implementation of schema stitching for these schemas might look like this: ```js -const mergedSchema = mergeSchemas({ - schemas: [ - chirpSchema, - authorSchema, - linkTypeDefs, +const schema = mergeSchemas({ + subschemas: [ + { schema: chirpSchema, }, + { schema: authorSchema, }, ], + typeDefs: linkTypeDefs, resolvers: { User: { chirps: { @@ -172,13 +170,9 @@ const mergedSchema = mergeSchemas({ }); ``` -[Run the above example on Launchpad.](https://launchpad.graphql.com/8r11mk9jq) - ## Using with Transforms -Often, when creating a GraphQL gateway that combines multiple existing schemas, we might want to modify one of the schemas. The most common tasks include renaming some of the types, and filtering the root fields. By using [transforms](/schema-transforms/) with schema stitching, we can easily tweak the subschemas before merging them together. - -Before, when we were simply merging schemas without first transforming them, we would typically delegate directly to one of the merged schemas. Once we add transforms to the mix, there are times when we want to delegate to fields of the new, transformed schemas, and other times when we want to delegate to the original, untransformed schemas. +Often, when creating a GraphQL gateway that combines multiple existing schemas, we might want to modify one of the schemas. The most common tasks include renaming some of the types, and filtering the root fields. By using [transforms](/schema-transforms/) with schema stitching, we can easily tweak the subschemas before merging them together. (In earlier versions of graphql-tools, this required an additional round of delegation prior to merging, but transforms can now be specifying directly when merging using the new subschema configuration objects.) For example, suppose we transform the `chirpSchema` by removing the `chirpsByAuthorId` field and add a `Chirp_` prefix to all types and field names, in order to make it very clear which types and fields came from `chirpSchema`: @@ -187,7 +181,6 @@ import { makeExecutableSchema, addMockFunctionsToSchema, mergeSchemas, - transformSchema, FilterRootFields, RenameTypes, RenameRootFields, @@ -213,35 +206,41 @@ const chirpSchema = makeExecutableSchema({ addMockFunctionsToSchema({ schema: chirpSchema }); -// create transform schema +// create transforms -const transformedChirpSchema = transformSchema(chirpSchema, [ +const chirpSchemaTransforms = [ new FilterRootFields( (operation: string, rootField: string) => rootField !== 'chirpsByAuthorId' ), new RenameTypes((name: string) => `Chirp_${name}`), new RenameRootFields((operation: 'Query' | 'Mutation' | 'Subscription', name: string) => `Chirp_${name}`), -]); +]; ``` -Now we have a schema that has all fields and types prefixed with `Chirp_` and has only the `chirpById` root field. Note that the original schema has not been modified, and remains fully functional. We've simply created a new, slightly different schema, which hopefully will be more convenient for merging with our other subschemas. +We will now have a schema that has all fields and types prefixed with `Chirp_` and has only the `chirpById` root field. Now let's implement the resolvers: -```js -const mergedSchema = mergeSchemas({ - schemas: [ - transformedChirpSchema, - authorSchema, - linkTypeDefs, +```ts +const chirpSubschema = { + schema: chirpSchema, + transforms: chirpSchemaTransforms, +} + +export const schema = mergeSchemas({ + subschemas: [ + chirpSubschema, + { schema: authorSchema }, ], + typeDefs: linkTypeDefs, + resolvers: { User: { chirps: { fragment: `... on User { id }`, resolve(user, args, context, info) { return delegateToSchema({ - schema: chirpSchema, + schema: chirpSubschema, operation: 'query', fieldName: 'chirpsByAuthorId', args: { @@ -249,7 +248,6 @@ const mergedSchema = mergeSchemas({ }, context, info, - transforms: transformedChirpSchema.transforms, }); }, }, @@ -277,23 +275,56 @@ const mergedSchema = mergeSchemas({ Notice that `resolvers.Chirp_Chirp` has been renamed from just `Chirp`, but `resolvers.Chirp_Chirp.author.fragment` still refers to the original `Chirp` type and `authorId` field, rather than `Chirp_Chirp` and `Chirp_authorId`. -Also, when we call `delegateToSchema` in the `User.chirps` resolvers, we can delegate to the original `chirpsByAuthorId` field, even though it has been filtered out of the final schema. That's because we're delegating to the original `chirpSchema`, which has not been modified by the transforms. +Also, when we call `delegateToSchema` in the `User.chirps` resolvers, we can delegate to the original `chirpsByAuthorId` field, even though it has been filtered out of the final schema. -## Complex example +## Working with remote schemas -For a more complicated example involving properties and bookings, with implementations of all of the resolvers, check out the Launchpad links below: +In order to merge with a remote schema, we specify different options within the subschema configuration object that describe how to connect to the remote schema. For example: -* [Property schema](https://launchpad.graphql.com/v7l45qkw3) -* [Booking schema](https://launchpad.graphql.com/41p4j4309) -* [Merged schema](https://launchpad.graphql.com/q5kq9z15p) +```ts + subschemas: [ + { + schema: nonExecutableChirpSchema, + link: chirpSchemaLink + transforms: chirpSchemaTransforms, + }, + { schema: authorSchema }, + ], +``` + +The remote schema may be obtained either via introspection or any other source. A link is a generic ApolloLink method of connecting to a schema, also used by Apollo Client. + +Specifying the remote schema options within the `mergeSchemas` call itself allows for skipping an additional round of delegation. The old method of using [makeRemoteExecutableSchema](/remote-schemas/) to create a local proxy for the remote schema would still work, and the same arguments are supported. See the [remote schema](/remote-schemas/) docs for further description of the options available. Subschema configuration allows for specifying an ApolloLink `link`, any fetcher method (if not using subscriptions), or a dispatcher function that takes the graphql `context` object as an argument and dynamically returns a link object or fetcher method. ## API -### mergeSchemas +### schemas ```ts + +export type SubschemaConfig = { + schema: GraphQLSchema; + rootValue?: Record; + executor?: Delegator; + subscriber?: Delegator; + link?: ApolloLink; + fetcher?: Fetcher; + dispatcher?: Dispatcher; + transforms?: Array; +}; + +export type SchemaLikeObject = + SubschemaConfig | + GraphQLSchema | + string | + DocumentNode | + Array; + mergeSchemas({ - schemas: Array>; + subschemas: Array; + types: Array; + typeDefs: string | DocumentNode; + schemas: Array; resolvers?: Array | IResolvers; onTypeConflict?: ( left: GraphQLNamedType, @@ -316,7 +347,7 @@ This is the main function that implements schema stitching. Read below for a des #### schemas -`schemas` is an array of `GraphQLSchema` objects, schema strings, or lists of `GraphQLNamedType`s. Strings can contain type extensions or GraphQL types, which will be added to resulting schema. Note that type extensions are always applied last, while types are defined in the order in which they are provided. +`schemas` is an array of `GraphQLSchema` objects, schema strings, or lists of `GraphQLNamedType`s. Strings can contain type extensions or GraphQL types, which will be added to resulting schema. Note that type extensions are always applied last, while types are defined in the order in which they are provided. Using the `subschemas` and `typeDefs` parameters is preferred, as these parameter names better describe whether the includes types will be wrapped or will be imported directly into the outer schema. #### resolvers @@ -344,14 +375,12 @@ resolvers: { } ``` -#### mergeInfo and delegateToSchema +#### delegateToSchema -The `info.mergeInfo` object provides the `delegateToSchema` method: +The `delegateToSchema` method: ```js -type MergeInfo = { - delegateToSchema(options: IDelegateToSchemaOptions): any; -} +delegateToSchema(options: IDelegateToSchemaOptions): any; interface IDelegateToSchemaOptions GraphQLSchema; transformRequest?: (originalRequest: Request) => Request; transformResult?: (result: Result) => Result; - resolversTransformResult?: boolean; }; export interface IGraphQLToolsResolveInfo extends GraphQLResolveInfo { @@ -75,47 +74,39 @@ export interface IFetcherOperation { export type Dispatcher = (context: any) => ApolloLink | Fetcher; -export type SchemaExecutionConfig = { +export type SubschemaConfig = { schema: GraphQLSchemaWithTransforms; + rootValue?: Record; + executor?: Delegator; + subscriber?: Delegator; link?: ApolloLink; fetcher?: Fetcher; dispatcher?: Dispatcher; -}; - -export type SubSchemaConfig = { transforms?: Array; -} & SchemaExecutionConfig; +}; export type GraphQLSchemaWithTransforms = GraphQLSchema & { transforms?: Array }; export type SchemaLikeObject = - SubSchemaConfig | + SubschemaConfig | GraphQLSchema | string | DocumentNode | Array; -export function isSchemaExecutionConfig(value: SchemaLikeObject): value is SchemaExecutionConfig { - return !!(value as SchemaExecutionConfig).schema; -} - -export function isSubSchemaConfig(value: SchemaLikeObject): value is SubSchemaConfig { - return !!(value as SubSchemaConfig).schema; +export function isSubschemaConfig(value: SchemaLikeObject): value is SubschemaConfig { + return !!(value as SubschemaConfig).schema; } export interface IDelegateToSchemaOptions { - schema: GraphQLSchema | SchemaExecutionConfig; - link?: ApolloLink; - fetcher?: Fetcher; - dispatcher?: Dispatcher; + schema: GraphQLSchema | SubschemaConfig; operation: Operation; fieldName: string; args?: { [key: string]: any }; context: TContext; info: IGraphQLToolsResolveInfo; + rootValue?: Record; transforms?: Array; skipValidation?: boolean; - executor?: Delegator; - subscriber?: Delegator; } export type Delegator = ({ document, context, variables }: { diff --git a/src/stitching/addTypenameToAbstract.ts b/src/stitching/addTypenameToAbstract.ts index 706574b1b32..9b957f6ba0b 100644 --- a/src/stitching/addTypenameToAbstract.ts +++ b/src/stitching/addTypenameToAbstract.ts @@ -9,11 +9,11 @@ import { FieldNode, GraphQLInterfaceType, GraphQLUnionType, + GraphQLSchema, } from 'graphql'; -import { GraphQLSchemaWithTransforms } from '../Interfaces'; export function addTypenameToAbstract( - targetSchema: GraphQLSchemaWithTransforms, + targetSchema: GraphQLSchema, document: DocumentNode, ): DocumentNode { const typeInfo = new TypeInfo(targetSchema); diff --git a/src/stitching/delegateToSchema.ts b/src/stitching/delegateToSchema.ts index 73cdddc4983..f875012d15d 100644 --- a/src/stitching/delegateToSchema.ts +++ b/src/stitching/delegateToSchema.ts @@ -22,7 +22,8 @@ import { Request, Fetcher, Delegator, - isSchemaExecutionConfig, + SubschemaConfig, + isSubschemaConfig, } from '../Interfaces'; import { @@ -55,24 +56,33 @@ export default function delegateToSchema( return delegateToSchemaImplementation(options); } -async function delegateToSchemaImplementation( - options: IDelegateToSchemaOptions, +async function delegateToSchemaImplementation({ + schema: schemaOrSubschemaConfig, + rootValue, + info, + operation = info.operation.operation, + fieldName, + args, + context, + transforms = [], + skipValidation, +}: IDelegateToSchemaOptions, ): Promise { - const { schema: schemaOrSchemaConfig } = options; - let targetSchema; - if (isSchemaExecutionConfig(schemaOrSchemaConfig)) { - targetSchema = schemaOrSchemaConfig.schema; - options.link = schemaOrSchemaConfig.link; - options.fetcher = schemaOrSchemaConfig.fetcher; - options.dispatcher = schemaOrSchemaConfig.dispatcher; + let targetSchema: GraphQLSchema; + let subSchemaConfig: SubschemaConfig; + + if (isSubschemaConfig(schemaOrSubschemaConfig)) { + subSchemaConfig = schemaOrSubschemaConfig; + targetSchema = subSchemaConfig.schema; + rootValue = rootValue || subSchemaConfig.rootValue || info.rootValue; + transforms = transforms.concat((subSchemaConfig.transforms || []).slice().reverse()); } else { - targetSchema = schemaOrSchemaConfig; + targetSchema = schemaOrSubschemaConfig; + rootValue = rootValue || info.rootValue; } - const { info } = options; - const operation = options.operation || info.operation.operation; const rawDocument: DocumentNode = createDocument( - options.fieldName, + fieldName, operation, info.fieldNodes, Object.keys(info.fragments).map( @@ -87,9 +97,9 @@ async function delegateToSchemaImplementation( variables: info.variableValues as Record, }; - let transforms = [ - new CheckResultAndHandleErrors(info, options.fieldName), - ...(options.transforms || []), + transforms = [ + new CheckResultAndHandleErrors(info, fieldName), + ...transforms, new ExpandAbstractTypes(info.schema, targetSchema), ]; @@ -99,7 +109,6 @@ async function delegateToSchemaImplementation( ); } - const { args } = options; if (args) { transforms.push( new AddArgumentsAsVariables(targetSchema, args) @@ -113,7 +122,7 @@ async function delegateToSchemaImplementation( const processedRequest = applyRequestTransforms(rawRequest, transforms); - if (!options.skipValidation) { + if (!skipValidation) { const errors = validate(targetSchema, processedRequest.document); if (errors.length > 0) { throw errors; @@ -121,24 +130,23 @@ async function delegateToSchemaImplementation( } if (operation === 'query' || operation === 'mutation') { - options.executor = options.executor || getExecutor(targetSchema, options); + const executor = createExecutor(targetSchema, rootValue, subSchemaConfig); return applyResultTransforms( - await options.executor({ + await executor({ document: processedRequest.document, - context: options.context, + context, variables: processedRequest.variables }), transforms, ); - } - if (operation === 'subscription') { - options.subscriber = options.subscriber || getSubscriber(targetSchema, options); + } else if (operation === 'subscription') { + const subscriber = createSubscriber(targetSchema, rootValue, subSchemaConfig); - const originalAsyncIterator = (await options.subscriber({ + const originalAsyncIterator = (await subscriber({ document: processedRequest.document, - context: options.context, + context, variables: processedRequest.variables, })) as AsyncIterator; @@ -211,17 +219,27 @@ function createDocument( }; } -function getExecutor(schema: GraphQLSchema, options: IDelegateToSchemaOptions): Delegator { +function createExecutor( + schema: GraphQLSchema, + rootValue: Record, + subSchemaConfig?: SubschemaConfig +): Delegator { let fetcher: Fetcher; - if (options.dispatcher) { - const dynamicLinkOrFetcher = options.dispatcher(context); - fetcher = (typeof dynamicLinkOrFetcher === 'function') ? - dynamicLinkOrFetcher : - linkToFetcher(dynamicLinkOrFetcher); - } else if (options.link) { - fetcher = linkToFetcher(options.link); - } else if (options.fetcher) { - fetcher = options.fetcher; + if (subSchemaConfig) { + if (subSchemaConfig.dispatcher) { + const dynamicLinkOrFetcher = subSchemaConfig.dispatcher(context); + fetcher = (typeof dynamicLinkOrFetcher === 'function') ? + dynamicLinkOrFetcher : + linkToFetcher(dynamicLinkOrFetcher); + } else if (subSchemaConfig.link) { + fetcher = linkToFetcher(subSchemaConfig.link); + } else if (subSchemaConfig.fetcher) { + fetcher = subSchemaConfig.fetcher; + } + + if (!fetcher && !rootValue && subSchemaConfig.rootValue) { + rootValue = subSchemaConfig.rootValue; + } } if (fetcher) { @@ -234,19 +252,30 @@ function getExecutor(schema: GraphQLSchema, options: IDelegateToSchemaOptions): return ({ document, context, variables }) => execute({ schema, document, - rootValue: options.info.rootValue, + rootValue, contextValue: context, variableValues: variables, }); } } -function getSubscriber(schema: GraphQLSchema, options: IDelegateToSchemaOptions): Delegator { +function createSubscriber( + schema: GraphQLSchema, + rootValue: Record, + subSchemaConfig?: SubschemaConfig +): Delegator { let link: ApolloLink; - if (options.dispatcher) { - link = options.dispatcher(context) as ApolloLink; - } else if (options.link) { - link = options.link; + + if (subSchemaConfig) { + if (subSchemaConfig.dispatcher) { + link = subSchemaConfig.dispatcher(context) as ApolloLink; + } else if (subSchemaConfig.link) { + link = subSchemaConfig.link; + } + + if (!link && !rootValue && subSchemaConfig.rootValue) { + rootValue = subSchemaConfig.rootValue; + } } if (link) { @@ -263,9 +292,9 @@ function getSubscriber(schema: GraphQLSchema, options: IDelegateToSchemaOptions) return ({ document, context, variables }) => subscribe({ schema, document, - rootValue: options.info.rootValue, + rootValue, contextValue: context, variableValues: variables, }); - } + } } diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 0fa8ed78ed3..a6178e4dd40 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -65,10 +65,9 @@ export default function makeRemoteExecutableSchema({ } } - const resolvers = generateProxyingResolvers(remoteSchema, [], createProxyingResolver); addResolveFunctionsToSchema({ schema: remoteSchema, - resolvers, + resolvers: generateProxyingResolvers({ schema: remoteSchema }, createProxyingResolver), resolverValidationOptions: { allowResolversNotInSchema: true, }, diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 990a24bb527..8ec23b71ae1 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -17,9 +17,10 @@ import { MergeInfo, OnTypeConflict, IResolversParameter, - isSubSchemaConfig, + isSubschemaConfig, SchemaLikeObject, IResolvers, + SubschemaConfig, } from '../Interfaces'; import { extractExtensionDefinitions, @@ -52,14 +53,20 @@ type CandidateSelector = ( ) => MergeTypeCandidate; export default function mergeSchemas({ - schemas, + subschemas = [], + types = [], + typeDefs, + schemas: schemaLikeObjects = [], onTypeConflict, resolvers, schemaDirectives, inheritResolversFromInterfaces, mergeDirectives, }: { - schemas: Array; + subschemas?: Array; + types?: Array; + typeDefs?: string | DocumentNode; + schemas?: Array; onTypeConflict?: OnTypeConflict; resolvers?: IResolversParameter; schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor }; @@ -68,22 +75,25 @@ export default function mergeSchemas({ }): GraphQLSchema { const allSchemas: Array = []; const typeCandidates: { [name: string]: Array } = {}; - const types: { [name: string]: GraphQLNamedType } = {}; + const typeMap: { [name: string]: GraphQLNamedType } = {}; const extensions: Array = []; const directives: Array = []; const fragments: Array<{ field: string; fragment: string; }> = []; + let schemas: Array = [...subschemas]; + if (typeDefs) { + schemas.push(typeDefs); + } + if (types) { + schemas.push(types); + } + schemas = [...schemas, ...schemaLikeObjects]; schemas.forEach(schemaLikeObject => { - if (schemaLikeObject instanceof GraphQLSchema || isSubSchemaConfig(schemaLikeObject)) { - let schema: GraphQLSchema; - if (isSubSchemaConfig(schemaLikeObject)) { - schema = wrapSchema(schemaLikeObject, schemaLikeObject.transforms || []); - } else { - schema = wrapSchema(schemaLikeObject, []); - } + if (schemaLikeObject instanceof GraphQLSchema || isSubschemaConfig(schemaLikeObject)) { + const schema = wrapSchema(schemaLikeObject); allSchemas.push(schema); @@ -109,9 +119,9 @@ export default function mergeSchemas({ }); } - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type: GraphQLNamedType = typeMap[typeName]; + const originalTypeMap = schema.getTypeMap(); + Object.keys(originalTypeMap).forEach(typeName => { + const type: GraphQLNamedType = originalTypeMap[typeName]; if ( isNamedType(type) && getNamedType(type).name.slice(0, 2) !== '__' && @@ -130,7 +140,7 @@ export default function mergeSchemas({ (schemaLikeObject && (schemaLikeObject as DocumentNode).kind === Kind.DOCUMENT) ) { let parsedSchemaDocument = - typeof schemaLikeObject === 'string' ? parse(schemaLikeObject) : (schemaLikeObject as DocumentNode); + typeof schemaLikeObject === 'string' ? parse(schemaLikeObject) : (schemaLikeObject as DocumentNode); parsedSchemaDocument.definitions.forEach(def => { const type = typeFromAST(def); if (type instanceof GraphQLDirective && mergeDirectives) { @@ -181,20 +191,20 @@ export default function mergeSchemas({ } Object.keys(typeCandidates).forEach(typeName => { - types[typeName] = mergeTypeCandidates( + typeMap[typeName] = mergeTypeCandidates( typeName, typeCandidates[typeName], onTypeConflict ? onTypeConflictToCandidateSelector(onTypeConflict) : undefined ); }); - healTypes(types, directives, { skipPruning: true }); + healTypes(typeMap, directives, { skipPruning: true }); let mergedSchema = new GraphQLSchema({ - query: types.Query as GraphQLObjectType, - mutation: types.Mutation as GraphQLObjectType, - subscription: types.Subscription as GraphQLObjectType, - types: Object.keys(types).map(key => types[key]), + query: typeMap.Query as GraphQLObjectType, + mutation: typeMap.Mutation as GraphQLObjectType, + subscription: typeMap.Subscription as GraphQLObjectType, + types: Object.keys(typeMap).map(key => typeMap[key]), directives: directives.length ? directives.map((directive) => cloneDirective(directive)) : undefined diff --git a/src/stitching/resolvers.ts b/src/stitching/resolvers.ts index 353b848205f..c8dbf5d1f1a 100644 --- a/src/stitching/resolvers.ts +++ b/src/stitching/resolvers.ts @@ -6,12 +6,10 @@ import { import { IResolvers, Operation, - SchemaExecutionConfig, - isSchemaExecutionConfig, + SubschemaConfig, } from '../Interfaces'; import delegateToSchema from './delegateToSchema'; import { makeMergedType } from './makeMergedType'; -import { Transform } from '../transforms/index'; export type Mapping = { [typeName: string]: { @@ -23,17 +21,14 @@ export type Mapping = { }; export function generateProxyingResolvers( - schemaOrSchemaExecutionConfig: GraphQLSchema | SchemaExecutionConfig, - transforms: Array = [], + subschemaConfig: SubschemaConfig, createProxyingResolver: ( - schema: GraphQLSchema | SchemaExecutionConfig, + schema: GraphQLSchema | SubschemaConfig, operation: Operation, fieldName: string, - transforms: Array, ) => GraphQLFieldResolver = defaultCreateProxyingResolver, ): IResolvers { - const targetSchema: GraphQLSchema = isSchemaExecutionConfig(schemaOrSchemaExecutionConfig) ? - schemaOrSchemaExecutionConfig.schema : schemaOrSchemaExecutionConfig; + const targetSchema = subschemaConfig.schema; const mapping = generateSimpleMapping(targetSchema); @@ -47,10 +42,9 @@ export function generateProxyingResolvers( to.operation === 'subscription' ? 'subscribe' : 'resolve'; result[name][from] = { [resolverType]: createProxyingResolver( - schemaOrSchemaExecutionConfig, + subschemaConfig, to.operation, to.name, - transforms, ), }; }); @@ -101,19 +95,17 @@ export function generateMappingFromObjectType( } function defaultCreateProxyingResolver( - schema: GraphQLSchema | SchemaExecutionConfig, + subschemaConfig: SubschemaConfig, operation: Operation, fieldName: string, - transforms: Array, ): GraphQLFieldResolver { return (parent, args, context, info) => delegateToSchema({ - schema, + schema: subschemaConfig, operation, fieldName, args, context, info, - transforms, }); } diff --git a/src/test/testAlternateMergeSchemas.ts b/src/test/testAlternateMergeSchemas.ts index 68312cbca16..7bd4fb2536e 100644 --- a/src/test/testAlternateMergeSchemas.ts +++ b/src/test/testAlternateMergeSchemas.ts @@ -43,7 +43,7 @@ import { createMergedResolver, extractFields, } from '../stitching'; -import { SchemaExecutionConfig } from '../Interfaces'; +import { SubschemaConfig } from '../Interfaces'; import isSpecifiedScalarType from '../utils/isSpecifiedScalarType'; function renameFieldNode(fieldNode: FieldNode, name: string): FieldNode { @@ -121,11 +121,11 @@ let linkSchema = ` `; describe('merge schemas through transforms', () => { - let bookingSchemaExecConfig: SchemaExecutionConfig; + let bookingSubschemaConfig: SubschemaConfig; let mergedSchema: GraphQLSchema; before(async () => { - bookingSchemaExecConfig = await remoteBookingSchema; + bookingSubschemaConfig = await remoteBookingSchema; // namespace and strip schemas const propertySchemaTransforms = [ @@ -144,7 +144,7 @@ describe('merge schemas through transforms', () => { new RenameTypes((name: string) => `Bookings_${name}`), new RenameRootFields((operation: string, name: string) => `Bookings_${name}`), ]; - const subScriptionSchemaTransforms = [ + const subscriptionSchemaTransforms = [ new FilterRootFields( (operation: string, rootField: string) => // must include a Query type otherwise graphql will error @@ -156,55 +156,59 @@ describe('merge schemas through transforms', () => { (operation: string, name: string) => `Subscriptions_${name}`), ]; + const propertySubschema = { + schema: propertySchema, + transforms: propertySchemaTransforms, + }; + const bookingSubschema = { + ...bookingSubschemaConfig, + transforms: bookingSchemaTransforms, + }; + const subscriptionSubschema = { + schema: subscriptionSchema, + transforms: subscriptionSchemaTransforms, + }; + mergedSchema = mergeSchemas({ - schemas: [ - { - schema: propertySchema, - transforms: propertySchemaTransforms, - }, - { - ...bookingSchemaExecConfig, - transforms: bookingSchemaTransforms, - }, - { - schema: subscriptionSchema, - transforms: subScriptionSchemaTransforms, - }, - linkSchema, + subschemas: [ + propertySubschema, + bookingSubschema, + subscriptionSubschema, ], + typeDefs: linkSchema, resolvers: { Query: { // delegating directly, no subschemas or mergeInfo node(parent, args, context, info) { if (args.id.startsWith('p')) { return info.mergeInfo.delegateToSchema({ - schema: propertySchema, + schema: propertySubschema, operation: 'query', fieldName: 'propertyById', args, context, info, - transforms: propertySchemaTransforms, + transforms: [], }); } else if (args.id.startsWith('b')) { return delegateToSchema({ - ...bookingSchemaExecConfig, + schema: bookingSubschema, operation: 'query', fieldName: 'bookingById', args, context, info, - transforms: bookingSchemaTransforms, + transforms: [], }); } else if (args.id.startsWith('c')) { return delegateToSchema({ - ...bookingSchemaExecConfig, + schema: bookingSubschema, operation: 'query', fieldName: 'customerById', args, context, info, - transforms: bookingSchemaTransforms, + transforms: [], }); } else { throw new Error('invalid id'); @@ -216,7 +220,7 @@ describe('merge schemas through transforms', () => { fragment: 'fragment PropertyFragment on Property { id }', resolve(parent, args, context, info) { return delegateToSchema({ - ...bookingSchemaExecConfig, + schema: bookingSubschema, operation: 'query', fieldName: 'bookingsByPropertyId', args: { @@ -225,7 +229,6 @@ describe('merge schemas through transforms', () => { }, context, info, - transforms: bookingSchemaTransforms, }); }, }, @@ -235,7 +238,7 @@ describe('merge schemas through transforms', () => { fragment: 'fragment BookingFragment on Booking { propertyId }', resolve(parent, args, context, info) { return info.mergeInfo.delegateToSchema({ - schema: propertySchema, + schema: propertySubschema, operation: 'query', fieldName: 'propertyById', args: { @@ -243,7 +246,6 @@ describe('merge schemas through transforms', () => { }, context, info, - transforms: propertySchemaTransforms, }); }, }, diff --git a/src/test/testMakeRemoteExecutableSchema.ts b/src/test/testMakeRemoteExecutableSchema.ts index 0e30c64172a..79fe440cd6b 100644 --- a/src/test/testMakeRemoteExecutableSchema.ts +++ b/src/test/testMakeRemoteExecutableSchema.ts @@ -15,10 +15,10 @@ import { makeRemoteExecutableSchema } from '../stitching'; describe('remote queries', () => { let schema: GraphQLSchema; before(async () => { - const remoteSchemaExecConfig = await makeSchemaRemoteFromLink(propertySchema); + const remoteSubschemaConfig = await makeSchemaRemoteFromLink(propertySchema); schema = makeRemoteExecutableSchema({ - schema: remoteSchemaExecConfig.schema, - link: remoteSchemaExecConfig.link + schema: remoteSubschemaConfig.schema, + link: remoteSubschemaConfig.link }); }); @@ -56,10 +56,10 @@ describe('remote queries', () => { describe('remote subscriptions', () => { let schema: GraphQLSchema; before(async () => { - const remoteSchemaExecConfig = await makeSchemaRemoteFromLink(subscriptionSchema); + const remoteSubschemaConfig = await makeSchemaRemoteFromLink(subscriptionSchema); schema = makeRemoteExecutableSchema({ - schema: remoteSchemaExecConfig.schema, - link: remoteSchemaExecConfig.link + schema: remoteSubschemaConfig.schema, + link: remoteSubschemaConfig.link }); }); diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index 648a8dcccf9..9d4ca781721 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -31,7 +31,7 @@ import { forAwaitEach } from 'iterall'; import { makeExecutableSchema } from '../makeExecutableSchema'; import { IResolvers, - SchemaExecutionConfig, + SubschemaConfig, } from '../Interfaces'; import { delegateToSchema } from '../stitching'; import { cloneSchema } from '../utils'; @@ -335,9 +335,9 @@ let schemaDirectiveTypeDefs = ` testCombinations.forEach(async combination => { describe('merging ' + combination.name, () => { let mergedSchema: GraphQLSchema, - propertySchema: GraphQLSchema | SchemaExecutionConfig, - productSchema: GraphQLSchema | SchemaExecutionConfig, - bookingSchema: GraphQLSchema | SchemaExecutionConfig; + propertySchema: GraphQLSchema | SubschemaConfig, + productSchema: GraphQLSchema | SubschemaConfig, + bookingSchema: GraphQLSchema | SubschemaConfig; before(async () => { propertySchema = await combination.property; diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index f5d0c085642..4d24f36f957 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -21,7 +21,7 @@ import { makeExecutableSchema } from '../makeExecutableSchema'; import { IResolvers, Fetcher, - SchemaExecutionConfig, + SubschemaConfig, } from '../Interfaces'; import introspectSchema from '../stitching/introspectSchema'; import { PubSub } from 'graphql-subscriptions'; @@ -762,7 +762,7 @@ function makeLinkFromSchema(schema: GraphQLSchema) { } export async function makeSchemaRemoteFromLink(schema: GraphQLSchema) - : Promise { + : Promise { const link = makeLinkFromSchema(schema); const clientSchema = await introspectSchema(link); return { @@ -772,7 +772,7 @@ export async function makeSchemaRemoteFromLink(schema: GraphQLSchema) } export async function makeSchemaRemoteFromDispatchedLink(schema: GraphQLSchema) - : Promise { + : Promise { const link = makeLinkFromSchema(schema); const clientSchema = await introspectSchema(link); return { @@ -783,7 +783,7 @@ export async function makeSchemaRemoteFromDispatchedLink(schema: GraphQLSchema) // ensure fetcher support exists from the 2.0 api async function makeExecutableSchemaFromDispatchedFetcher(schema: GraphQLSchema) - : Promise { + : Promise { const fetcher: Fetcher = ({ query, operationName, variables, context }) => { return graphql( schema, diff --git a/src/transforms/RenameTypes.ts b/src/transforms/RenameTypes.ts index c3afab33583..ab1bd5e180b 100644 --- a/src/transforms/RenameTypes.ts +++ b/src/transforms/RenameTypes.ts @@ -5,12 +5,10 @@ import { Kind, GraphQLNamedType, GraphQLScalarType, - GraphQLAbstractType, } from 'graphql'; import isSpecifiedScalarType from '../utils/isSpecifiedScalarType'; import { Request, Result, VisitSchemaKind } from '../Interfaces'; import { Transform } from '../transforms/transforms'; -import { isParentProxiedResult } from '../stitching/errors'; import { visitSchema, cloneType } from '../utils'; export type RenameOptions = { @@ -19,8 +17,6 @@ export type RenameOptions = { }; export default class RenameTypes implements Transform { - public readonly resolversTransformResult = true; - private renamer: (name: string) => string | undefined; private reverseMap: { [key: string]: string }; private renameBuiltins: boolean; @@ -38,7 +34,7 @@ export default class RenameTypes implements Transform { } public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { - return visitSchema(originalSchema, [{ + return visitSchema(originalSchema, { [VisitSchemaKind.TYPE]: (type: GraphQLNamedType) => { if (isSpecifiedScalarType(type) && !this.renameBuiltins) { return undefined; @@ -58,20 +54,7 @@ export default class RenameTypes implements Transform { [VisitSchemaKind.ROOT_OBJECT](type: GraphQLNamedType) { return undefined; }, - }, { - [VisitSchemaKind.ABSTRACT_TYPE]: (type: GraphQLAbstractType) => { - const originalResolveType = type.resolveType; - type.resolveType = (value, info, context, abstractType) => { - if (isParentProxiedResult(value)) { - const oldName = originalResolveType(value, info, context, abstractType) as string; - const newName = this.renamer(oldName); - return newName ? newName : oldName; - } - return originalResolveType(value, info, context, abstractType); - }; - return type; - }, - }]); + }); } public transformRequest(originalRequest: Request): Request { diff --git a/src/transforms/transformSchema.ts b/src/transforms/transformSchema.ts index b50dbcf51bf..59589f40827 100644 --- a/src/transforms/transformSchema.ts +++ b/src/transforms/transformSchema.ts @@ -7,48 +7,48 @@ import { stripResolvers, } from '../stitching/resolvers'; import { - SchemaExecutionConfig, - isSchemaExecutionConfig, + SubschemaConfig, + isSubschemaConfig, } from '../Interfaces'; import { cloneSchema } from '../utils/clone'; export function wrapSchema( - schemaOrSchemaExecutionConfig: GraphQLSchema | SchemaExecutionConfig, - transforms: Array, + schemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig, + transforms: Array = [], ): GraphQLSchema { - const targetSchema: GraphQLSchema = isSchemaExecutionConfig(schemaOrSchemaExecutionConfig) ? - schemaOrSchemaExecutionConfig.schema : schemaOrSchemaExecutionConfig; + let subschemaConfig: SubschemaConfig; + if (isSubschemaConfig(schemaOrSubschemaConfig)) { + subschemaConfig = { + ...schemaOrSubschemaConfig, + transforms: (schemaOrSubschemaConfig.transforms || []).concat(transforms), + }; + } else { + subschemaConfig = { + schema: schemaOrSubschemaConfig, + transforms, + }; + } - const schema = cloneSchema(targetSchema); + const schema = cloneSchema(subschemaConfig.schema); stripResolvers(schema); - const resolvers = generateProxyingResolvers( - schemaOrSchemaExecutionConfig, - transforms.slice().reverse().map(transform => { - return transform.resolversTransformResult - ? { - transformRequest: originalRequest => transform.transformRequest(originalRequest) - } : - transform; - }), - ); addResolveFunctionsToSchema({ schema, - resolvers, + resolvers: generateProxyingResolvers(subschemaConfig), resolverValidationOptions: { allowResolversNotInSchema: true, }, }); - return applySchemaTransforms(schema, transforms); + return applySchemaTransforms(schema, subschemaConfig.transforms); } export default function transformSchema( - schemaOrSchemaExecutionConfig: GraphQLSchema | SchemaExecutionConfig, + schemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig, transforms: Array, ): GraphQLSchema & { transforms: Array } { - const schema = wrapSchema(schemaOrSchemaExecutionConfig, transforms); + const schema = wrapSchema(schemaOrSubschemaConfig, transforms); (schema as any).transforms = transforms.slice().reverse(); return schema as GraphQLSchema & { transforms: Array }; }