From e5d23fe2448ff245352735807e8b2480905ae655 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 3 Jun 2020 16:54:42 -0400 Subject: [PATCH] introduce Subschema class (#1583) * add compiled Subschema class for use with delegation outside of stitching context to ensure that transformSchema methods are called for all transforms closes #1565 * reorganize subschema functions into subschema file --- packages/delegate/src/Subschema.ts | 46 ++++++ .../delegate/src/defaultMergedResolver.ts | 2 +- packages/delegate/src/delegateToSchema.ts | 2 +- packages/delegate/src/index.ts | 2 +- packages/delegate/src/proxiedResult.ts | 2 +- packages/delegate/src/results/handleObject.ts | 4 +- packages/delegate/src/subschema.ts | 14 -- packages/delegate/src/types.ts | 8 +- packages/delegate/tests/transforms.test.ts | 133 ++++++++++++++++++ 9 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 packages/delegate/src/Subschema.ts delete mode 100644 packages/delegate/src/subschema.ts create mode 100644 packages/delegate/tests/transforms.test.ts diff --git a/packages/delegate/src/Subschema.ts b/packages/delegate/src/Subschema.ts new file mode 100644 index 00000000000..4eac7d34158 --- /dev/null +++ b/packages/delegate/src/Subschema.ts @@ -0,0 +1,46 @@ +import { GraphQLSchema } from 'graphql'; + +import { Transform, applySchemaTransforms } from '@graphql-tools/utils'; + +import { SubschemaConfig, MergedTypeConfig, CreateProxyingResolverFn, Subscriber, Executor } from './types'; + +import { FIELD_SUBSCHEMA_MAP_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL } from './symbols'; + +export function getSubschema(result: any, responseKey: string): GraphQLSchema | SubschemaConfig { + const subschema = result[FIELD_SUBSCHEMA_MAP_SYMBOL] && result[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey]; + return subschema || result[OBJECT_SUBSCHEMA_SYMBOL]; +} + +export function setObjectSubschema(result: any, subschema: GraphQLSchema | SubschemaConfig) { + result[OBJECT_SUBSCHEMA_SYMBOL] = subschema; +} + +export function isSubschemaConfig(value: any): value is SubschemaConfig { + return Boolean((value as SubschemaConfig).schema); +} + +export function isSubschema(value: any): value is Subschema { + return Boolean((value as Subschema).transformedSchema); +} + +export class Subschema { + public schema: GraphQLSchema; + public rootValue?: Record; + public executor?: Executor; + public subscriber?: Subscriber; + public createProxyingResolver?: CreateProxyingResolverFn; + public transforms: Array; + public transformedSchema: GraphQLSchema; + public merge?: Record; + + constructor(config: SubschemaConfig) { + this.schema = config.schema; + this.executor = config.executor; + this.subscriber = config.subscriber; + this.createProxyingResolver = config.createProxyingResolver; + this.transforms = config.transforms ?? []; + this.merge = config.merge; + + this.transformedSchema = applySchemaTransforms(this.schema, this.transforms); + } +} diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index a85837d2229..a023b1d3090 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -3,7 +3,7 @@ import { defaultFieldResolver, GraphQLResolveInfo } from 'graphql'; import { getResponseKeyFromInfo, getErrors } from '@graphql-tools/utils'; import { handleResult } from './results/handleResult'; -import { getSubschema } from './subschema'; +import { getSubschema } from './Subschema'; /** * Resolver that knows how to: diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 4d55f2f0af1..90d13414643 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -36,10 +36,10 @@ import { IDelegateToSchemaOptions, IDelegateRequestOptions, SubschemaConfig, - isSubschemaConfig, ExecutionParams, StitchingInfo, } from './types'; +import { isSubschemaConfig } from './Subschema'; export function delegateToSchema(options: IDelegateToSchemaOptions | GraphQLSchema): any { if (isSchema(options)) { diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 93f95bf2e0d..1ef0bbed5a1 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -3,7 +3,7 @@ export { createRequestFromInfo, createRequest } from './createRequest'; export { defaultMergedResolver } from './defaultMergedResolver'; export { createMergedResolver } from './createMergedResolver'; export { handleResult } from './results/handleResult'; -export { getSubschema } from './subschema'; +export { Subschema, isSubschema, isSubschemaConfig, getSubschema } from './Subschema'; export * from './transforms'; diff --git a/packages/delegate/src/proxiedResult.ts b/packages/delegate/src/proxiedResult.ts index 437432d18e3..f812d1e1db7 100644 --- a/packages/delegate/src/proxiedResult.ts +++ b/packages/delegate/src/proxiedResult.ts @@ -5,7 +5,7 @@ import { mergeDeep, ERROR_SYMBOL, relocatedError, setErrors, getErrors } from '@ import { handleNull } from './results/handleNull'; import { FIELD_SUBSCHEMA_MAP_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL } from './symbols'; -import { getSubschema, setObjectSubschema } from './subschema'; +import { getSubschema, setObjectSubschema } from './Subschema'; import { SubschemaConfig } from './types'; export function isProxiedResult(result: any) { diff --git a/packages/delegate/src/results/handleObject.ts b/packages/delegate/src/results/handleObject.ts index 3a0b6ae1b4d..51d18e0d66a 100644 --- a/packages/delegate/src/results/handleObject.ts +++ b/packages/delegate/src/results/handleObject.ts @@ -9,9 +9,9 @@ import { } from 'graphql'; import { collectFields, GraphQLExecutionContext, setErrors, slicedError } from '@graphql-tools/utils'; -import { setObjectSubschema } from '../subschema'; +import { setObjectSubschema, isSubschemaConfig } from '../Subschema'; import { mergeFields } from '../mergeFields'; -import { MergedTypeInfo, SubschemaConfig, isSubschemaConfig } from '../types'; +import { MergedTypeInfo, SubschemaConfig } from '../types'; export function handleObject( type: GraphQLCompositeType, diff --git a/packages/delegate/src/subschema.ts b/packages/delegate/src/subschema.ts deleted file mode 100644 index c85aac004e9..00000000000 --- a/packages/delegate/src/subschema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { GraphQLSchema } from 'graphql'; - -import { SubschemaConfig } from './types'; - -import { FIELD_SUBSCHEMA_MAP_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL } from './symbols'; - -export function getSubschema(result: any, responseKey: string): GraphQLSchema | SubschemaConfig { - const subschema = result[FIELD_SUBSCHEMA_MAP_SYMBOL] && result[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey]; - return subschema || result[OBJECT_SUBSCHEMA_SYMBOL]; -} - -export function setObjectSubschema(result: any, subschema: GraphQLSchema | SubschemaConfig) { - result[OBJECT_SUBSCHEMA_SYMBOL] = subschema; -} diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index fa115821913..3203cc82701 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -13,8 +13,10 @@ import { } from 'graphql'; import { Operation, Transform, Request, TypeMap, ExecutionResult } from '@graphql-tools/utils'; +import { Subschema } from './Subschema'; + export interface IDelegateToSchemaOptions, TArgs = Record> { - schema: GraphQLSchema | SubschemaConfig; + schema: GraphQLSchema | SubschemaConfig | Subschema; operation?: Operation; fieldName?: string; returnType?: GraphQLOutputType; @@ -123,10 +125,6 @@ export type MergedTypeResolver = ( selectionSet: SelectionSetNode ) => any; -export function isSubschemaConfig(value: any): value is SubschemaConfig { - return Boolean((value as SubschemaConfig).schema); -} - export interface StitchingInfo { transformedSchemas: Map; fragmentsByField: Record>; diff --git a/packages/delegate/tests/transforms.test.ts b/packages/delegate/tests/transforms.test.ts new file mode 100644 index 00000000000..34f51cf09a5 --- /dev/null +++ b/packages/delegate/tests/transforms.test.ts @@ -0,0 +1,133 @@ +import { Subschema, delegateToSchema } from '@graphql-tools/delegate'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { RenameRootFields, RenameObjectFields } from '@graphql-tools/wrap'; +import { graphql, GraphQLSchema } from 'graphql'; + +describe('can delegate to subschema with transforms', () => { + let sourceSchema: GraphQLSchema; + + beforeAll(() => { + const ITEM = { + id: '123', + // eslint-disable-next-line camelcase + camel_case: "I'm a camel!", + }; + + const targetSchema = makeExecutableSchema({ + typeDefs: ` + type Item { + id: ID! + camel_case: String + } + type ItemConnection { + edges: [ItemEdge!]! + } + type ItemEdge { + node: Item! + } + type Query { + item: Item + allItems: ItemConnection! + } + `, + resolvers: { + Query: { + item: () => ITEM, + allItems: () => ({ + edges: [ + { + node: ITEM, + }, + ], + }), + }, + }, + }); + + const subschema = new Subschema({ + schema: targetSchema, + transforms: [ + new RenameRootFields((_operation, fieldName) => { + if (fieldName === 'allItems') { + return 'items'; + } + return fieldName; + }), + new RenameObjectFields((_typeName, fieldName) => { + if (fieldName === 'camel_case') { + return 'camelCase'; + } + return fieldName; + }), + ] + }); + + sourceSchema = makeExecutableSchema({ + typeDefs: ` + type Item { + id: ID! + camelCase: String + } + type ItemConnection { + edges: [ItemEdge!]! + } + type ItemEdge { + node: Item! + } + type Query { + item: Item + items: ItemConnection! + } + `, + resolvers: { + Query: { + item: (_root, _args, _context, info) => delegateToSchema({ + schema: subschema, + info, + }), + items: (_root, _args, _context, info) => delegateToSchema({ + schema: subschema, + info, + }), + } + } + }) + }); + + test('renaming should work', async () => { + const result = await graphql( + sourceSchema, + ` + query { + item { + camelCase + } + items { + edges { + node { + camelCase + } + } + } + } + `, + ); + + const TRANSFORMED_ITEM = { + camelCase: "I'm a camel!", + }; + + expect(result).toEqual({ + data: { + item: TRANSFORMED_ITEM, + items: { + edges: [ + { + node: TRANSFORMED_ITEM, + }, + ], + }, + }, + }); + }); +});