From 5560feafe2c06c1c35e232a1beb2e5a1b80a9b7e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 26 Jul 2021 21:58:48 +0300 Subject: [PATCH] use lazy query planner refactor externalObject internals into separate file add tests from #2951 and fix them by reworking batch delegation buildDelegationPlan can be used to calculate the rounds of delegation necessary to completely merge an object given the stitching metadata stored within the schema and a given set of fieldNodes TODO: change buildDelegationPlan function to method on MergedTypeInfo as class? move back to stitch package? --- .../src/batchDelegateToSchema.ts | 50 +- .../src/createBatchDelegateFn.ts | 31 - packages/batch-delegate/src/getLoader.ts | 139 +++-- packages/batch-delegate/src/index.ts | 1 - packages/batch-delegate/src/types.ts | 18 +- .../batch-delegate/tests/errorPaths.test.ts | 153 +++++ .../tests/withTransforms.test.ts | 3 +- packages/delegate/src/Subschema.ts | 9 +- packages/delegate/src/Transformer.ts | 5 +- .../src/checkResultAndHandleErrors.ts | 111 ---- .../delegate/src/defaultMergedResolver.ts | 57 +- packages/delegate/src/delegateToSchema.ts | 77 ++- packages/delegate/src/externalObjects.ts | 70 +++ packages/delegate/src/externalValues.ts | 164 ++++++ packages/delegate/src/getMergedParent.ts | 555 ++++++++++++++++++ packages/delegate/src/index.ts | 5 +- packages/delegate/src/mergeDataAndErrors.ts | 73 +++ packages/delegate/src/mergeFields.ts | 123 ---- .../delegate/src/prepareGatewayDocument.ts | 2 +- packages/delegate/src/resolveExternalValue.ts | 188 ------ packages/delegate/src/symbols.ts | 8 +- packages/delegate/src/types.ts | 48 +- packages/delegate/tests/errors.test.ts | 55 +- packages/stitch/package.json | 1 - .../stitch/src/createDelegationPlanBuilder.ts | 239 -------- .../stitch/src/createMergedTypeResolver.ts | 13 +- packages/stitch/src/stitchSchemas.ts | 57 +- packages/stitch/src/stitchingInfo.ts | 7 +- .../splitMergedTypeEntryPointsTransformer.ts | 2 +- packages/stitch/src/types.ts | 1 - .../tests/alternateStitchSchemas.test.ts | 9 +- packages/stitch/tests/unknownType.test.ts | 8 +- .../src/stitchingDirectivesTransformer.ts | 12 +- packages/utils/src/memoize.ts | 97 ++- packages/wrap/package.json | 3 - .../wrap/src/generateProxyingResolvers.ts | 8 +- 36 files changed, 1496 insertions(+), 906 deletions(-) delete mode 100644 packages/batch-delegate/src/createBatchDelegateFn.ts create mode 100644 packages/batch-delegate/tests/errorPaths.test.ts delete mode 100644 packages/delegate/src/checkResultAndHandleErrors.ts create mode 100644 packages/delegate/src/externalObjects.ts create mode 100644 packages/delegate/src/externalValues.ts create mode 100644 packages/delegate/src/getMergedParent.ts create mode 100644 packages/delegate/src/mergeDataAndErrors.ts delete mode 100644 packages/delegate/src/mergeFields.ts delete mode 100644 packages/delegate/src/resolveExternalValue.ts delete mode 100644 packages/stitch/src/createDelegationPlanBuilder.ts diff --git a/packages/batch-delegate/src/batchDelegateToSchema.ts b/packages/batch-delegate/src/batchDelegateToSchema.ts index ba12dd310fb..e6acc20eabb 100644 --- a/packages/batch-delegate/src/batchDelegateToSchema.ts +++ b/packages/batch-delegate/src/batchDelegateToSchema.ts @@ -1,14 +1,60 @@ import { BatchDelegateOptions } from './types'; +import { getNullableType, GraphQLError, GraphQLList } from 'graphql'; + +import { externalValueFromResult } from '@graphql-tools/delegate'; +import { relocatedError } from '@graphql-tools/utils'; + import { getLoader } from './getLoader'; -export function batchDelegateToSchema(options: BatchDelegateOptions): any { +export async function batchDelegateToSchema(options: BatchDelegateOptions): Promise { const key = options.key; if (key == null) { return null; } else if (Array.isArray(key) && !key.length) { return []; } + + const { + schema, + info, + fieldName = info.fieldName, + returnType = info.returnType, + context, + onLocatedError = (originalError: GraphQLError) => + relocatedError(originalError, originalError.path ? originalError.path.slice(1) : []), + } = options; + const loader = getLoader(options); - return Array.isArray(key) ? loader.loadMany(key) : loader.load(key); + + if (Array.isArray(key)) { + const results = await loader.loadMany(key); + + return results.map(result => + result instanceof Error + ? result + : externalValueFromResult({ + result, + schema, + info, + context, + fieldName, + returnType: (getNullableType(returnType) as GraphQLList).ofType, + onLocatedError, + }) + ); + } + + const result = await loader.load(key); + return result instanceof Error + ? result + : externalValueFromResult({ + result, + schema, + info, + context, + fieldName, + returnType, + onLocatedError, + }); } diff --git a/packages/batch-delegate/src/createBatchDelegateFn.ts b/packages/batch-delegate/src/createBatchDelegateFn.ts deleted file mode 100644 index 5b335063023..00000000000 --- a/packages/batch-delegate/src/createBatchDelegateFn.ts +++ /dev/null @@ -1,31 +0,0 @@ -import DataLoader from 'dataloader'; - -import { CreateBatchDelegateFnOptions, BatchDelegateOptionsFn, BatchDelegateFn } from './types'; - -import { getLoader } from './getLoader'; - -export function createBatchDelegateFn( - optionsOrArgsFromKeys: CreateBatchDelegateFnOptions | ((keys: ReadonlyArray) => Record), - lazyOptionsFn?: BatchDelegateOptionsFn, - dataLoaderOptions?: DataLoader.Options, - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array -): BatchDelegateFn { - return typeof optionsOrArgsFromKeys === 'function' - ? createBatchDelegateFnImpl({ - argsFromKeys: optionsOrArgsFromKeys, - lazyOptionsFn, - dataLoaderOptions, - valuesFromResults, - }) - : createBatchDelegateFnImpl(optionsOrArgsFromKeys); -} - -function createBatchDelegateFnImpl(options: CreateBatchDelegateFnOptions): BatchDelegateFn { - return batchDelegateOptions => { - const loader = getLoader({ - ...options, - ...batchDelegateOptions, - }); - return loader.load(batchDelegateOptions.key); - }; -} diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index b3b910b71df..01be9a73093 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -1,48 +1,59 @@ -import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode } from 'graphql'; +import { GraphQLSchema, FieldNode } from 'graphql'; import DataLoader from 'dataloader'; -import { delegateToSchema, SubschemaConfig } from '@graphql-tools/delegate'; -import { memoize2, relocatedError } from '@graphql-tools/utils'; +import { + SubschemaConfig, + Transformer, + DelegationContext, + validateRequest, + getExecutor, + getDelegatingOperation, + createRequestFromInfo, + getDelegationContext, +} from '@graphql-tools/delegate'; +import { ExecutionRequest, ExecutionResult, memoize2 } from '@graphql-tools/utils'; import { BatchDelegateOptions } from './types'; -function createBatchFn(options: BatchDelegateOptions) { +function createBatchFn( + options: BatchDelegateOptions +): ( + keys: ReadonlyArray, + request: ExecutionRequest, + delegationContext: DelegationContext +) => Promise>>> { const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray) => ({ ids: keys })); - const fieldName = options.fieldName ?? options.info.fieldName; - const { valuesFromResults, lazyOptionsFn } = options; - - return async function batchFn(keys: ReadonlyArray) { - const results = await delegateToSchema({ - returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), - onLocatedError: originalError => { - if (originalError.path == null) { - return originalError; - } - - const [pathFieldName, pathNumber] = originalError.path; - - if (pathFieldName !== fieldName) { - return originalError; - } - const pathNumberType = typeof pathNumber; - if (pathNumberType !== 'number') { - return originalError; - } - - return relocatedError(originalError, originalError.path.slice(0, 0).concat(originalError.path.slice(2))); - }, + + const { validateRequest: shouldValidateRequest } = options; + + return async function batchFn( + keys: ReadonlyArray, + request: ExecutionRequest, + delegationContext: DelegationContext + ) { + const { fieldName, context, info } = delegationContext; + + const transformer = new Transformer({ + ...delegationContext, args: argsFromKeys(keys), - ...(lazyOptionsFn == null ? options : lazyOptionsFn(options)), }); - if (results instanceof Error) { - return keys.map(() => results); + const processedRequest = transformer.transformRequest(request); + + if (shouldValidateRequest) { + validateRequest(delegationContext, processedRequest.document); } - const values = valuesFromResults == null ? results : valuesFromResults(results, keys); + const executor = getExecutor(delegationContext); - return Array.isArray(values) ? values : keys.map(() => values); + const batchResult = (await executor({ + ...processedRequest, + context, + info, + })) as ExecutionResult; + + return splitResult(transformer.transformResult(batchResult), fieldName, keys.length); }; } @@ -60,23 +71,75 @@ const getLoadersMap = memoize2(function getLoadersMap( return new Map>(); }); -export function getLoader(options: BatchDelegateOptions): DataLoader { - const fieldName = options.fieldName ?? options.info.fieldName; - const loaders = getLoadersMap(options.info.fieldNodes, options.schema); +export function getLoader(options: BatchDelegateOptions): DataLoader { + const { + info, + operationName, + operation = getDelegatingOperation(info.parentType, info.schema), + fieldName = info.fieldName, + returnType = info.returnType, + selectionSet, + fieldNodes, + } = options; + + if (operation !== 'query' && operation !== 'mutation') { + throw new Error(`Batch delegation not possible for operation '${operation}'.`); + } - let loader = loaders.get(fieldName); + const request = createRequestFromInfo({ + info, + operation, + fieldName, + selectionSet, + fieldNodes, + operationName, + }); + + const delegationContext = getDelegationContext({ + request, + ...options, + operation, + fieldName, + returnType, + }); // Prevents the keys to be passed with the same structure - const dataLoaderOptions: DataLoader.Options = { + const dataLoaderOptions: DataLoader.Options = { cacheKeyFn: defaultCacheKeyFn, ...options.dataLoaderOptions, }; + const loaders = getLoadersMap(info.fieldNodes, options.schema); + let loader = loaders.get(fieldName); + if (loader === undefined) { const batchFn = createBatchFn(options); - loader = new DataLoader(batchFn, dataLoaderOptions); + loader = new DataLoader( + keys => batchFn(keys, request, delegationContext), + dataLoaderOptions + ); loaders.set(fieldName, loader); } return loader; } + +function splitResult(result: ExecutionResult, fieldName: string, numItems: number): Array { + const { data, errors } = result; + const fieldData = data?.[fieldName]; + + if (fieldData === undefined) { + if (errors === undefined) { + return Array(numItems).fill({}); + } + + return Array(numItems).fill({ errors }); + } + + return fieldData.map((value: any) => ({ + data: { + [fieldName]: value, + }, + errors, + })); +} diff --git a/packages/batch-delegate/src/index.ts b/packages/batch-delegate/src/index.ts index 2b26b57b77b..acc6780239a 100644 --- a/packages/batch-delegate/src/index.ts +++ b/packages/batch-delegate/src/index.ts @@ -1,4 +1,3 @@ export * from './batchDelegateToSchema'; -export * from './createBatchDelegateFn'; export * from './types'; diff --git a/packages/batch-delegate/src/types.ts b/packages/batch-delegate/src/types.ts index c9601b5541a..20f9fbcce9d 100644 --- a/packages/batch-delegate/src/types.ts +++ b/packages/batch-delegate/src/types.ts @@ -6,23 +6,15 @@ export type BatchDelegateFn, K = any> = ( batchDelegateOptions: BatchDelegateOptions ) => any; -export type BatchDelegateOptionsFn, K = any> = ( - batchDelegateOptions: BatchDelegateOptions -) => IDelegateToSchemaOptions; - -export interface BatchDelegateOptions, K = any, V = any, C = K> - extends Omit, 'args'> { +export interface CreateBatchDelegateFnOptions, K = any, V = any, C = K> + extends Partial, 'args' | 'info'>> { dataLoaderOptions?: DataLoader.Options; - key: K; argsFromKeys?: (keys: ReadonlyArray) => Record; - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array; - lazyOptionsFn?: BatchDelegateOptionsFn; } -export interface CreateBatchDelegateFnOptions, K = any, V = any, C = K> - extends Partial, 'args' | 'info'>> { +export interface BatchDelegateOptions, K = any, V = any, C = K> + extends Omit, 'args'> { dataLoaderOptions?: DataLoader.Options; + key: K | Array; argsFromKeys?: (keys: ReadonlyArray) => Record; - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array; - lazyOptionsFn?: (batchDelegateOptions: BatchDelegateOptions) => IDelegateToSchemaOptions; } diff --git a/packages/batch-delegate/tests/errorPaths.test.ts b/packages/batch-delegate/tests/errorPaths.test.ts new file mode 100644 index 00000000000..b85a5318c25 --- /dev/null +++ b/packages/batch-delegate/tests/errorPaths.test.ts @@ -0,0 +1,153 @@ +import { graphql, GraphQLError } from 'graphql'; +import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; +import { delegateToSchema } from '@graphql-tools/delegate'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stitchSchemas } from '@graphql-tools/stitch'; + +class NotFoundError extends GraphQLError { + constructor(id: unknown) { + super('Not Found', undefined, undefined, undefined, undefined, undefined, { id }); + } +} + +describe('preserves error path indices', () => { + const getProperty = jest.fn((id: unknown) => { + return new NotFoundError(id); + }); + + beforeEach(() => { + getProperty.mockClear(); + }); + + const subschema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Property { + id: ID! + } + + type Object { + id: ID! + propertyId: ID! + } + + type Query { + objects: [Object!]! + propertyById(id: ID!): Property + propertiesByIds(ids: [ID!]!): [Property]! + } + `, + resolvers: { + Query: { + objects: () => { + return [ + { id: '1', propertyId: '1' }, + { id: '2', propertyId: '1' }, + ]; + }, + propertyById: (_, args) => getProperty(args.id), + propertiesByIds: (_, args) => args.ids.map(getProperty), + }, + }, + }); + + const subschemas = [subschema]; + const typeDefs = /* GraphQL */ ` + extend type Object { + property: Property + } + `; + + const query = /* GraphQL */ ` + query { + objects { + id + property { + id + } + } + } + `; + + const expected = { + errors: [ + { + message: 'Not Found', + extensions: { id: '1' }, + path: ['objects', 0, 'property'], + }, + { + message: 'Not Found', + extensions: { id: '1' }, + path: ['objects', 1, 'property'], + }, + ], + data: { + objects: [ + { + id: '1', + property: null as null, + }, + { + id: '2', + property: null as null, + }, + ], + }, + }; + + test('using delegateToSchema', async () => { + const schema = stitchSchemas({ + subschemas, + typeDefs, + resolvers: { + Object: { + property: { + selectionSet: '{ propertyId }', + resolve: (source, _, context, info) => { + return delegateToSchema({ + schema: subschema, + fieldName: 'propertyById', + args: { id: source.propertyId }, + context, + info, + }); + }, + }, + }, + }, + }); + + const result = await graphql(schema, query); + + expect(getProperty).toBeCalledTimes(2); + expect(result).toMatchObject(expected); + }); + + test('using batchDelegateToSchema', async () => { + const schema = stitchSchemas({ + subschemas, + typeDefs, + resolvers: { + Object: { + property: { + selectionSet: '{ propertyId }', + resolve: (source, _, context, info) => { + return batchDelegateToSchema({ + schema: subschema, + fieldName: 'propertiesByIds', + key: source.propertyId, + context, + info, + }); + }, + }, + }, + }, + }); + + const result = await graphql(schema, query); + + expect(getProperty).toBeCalledTimes(1); + expect(result).toMatchObject(expected); + }); +}); diff --git a/packages/batch-delegate/tests/withTransforms.test.ts b/packages/batch-delegate/tests/withTransforms.test.ts index dfea2463c06..1ec2a1c0f22 100644 --- a/packages/batch-delegate/tests/withTransforms.test.ts +++ b/packages/batch-delegate/tests/withTransforms.test.ts @@ -1,4 +1,4 @@ -import { execute, GraphQLList, GraphQLObjectType, Kind, parse } from 'graphql'; +import { execute, Kind, parse } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; @@ -100,7 +100,6 @@ describe('works with complex transforms', () => { context, info, transforms: [queryTransform], - returnType: new GraphQLList(new GraphQLList(info.schema.getType('Book') as GraphQLObjectType)) }), }, }, diff --git a/packages/delegate/src/Subschema.ts b/packages/delegate/src/Subschema.ts index 668d68f82e6..6b963c7c000 100644 --- a/packages/delegate/src/Subschema.ts +++ b/packages/delegate/src/Subschema.ts @@ -9,13 +9,8 @@ export function isSubschema(value: any): value is Subschema { return Boolean(value.transformedSchema); } -interface ISubschema> - extends SubschemaConfig { - transformedSchema: GraphQLSchema; -} - export class Subschema> - implements ISubschema + implements SubschemaConfig { public schema: GraphQLSchema; @@ -27,7 +22,7 @@ export class Subschema> public transforms: Array>; public transformedSchema: GraphQLSchema; - public merge?: Record>; + public merge?: Record>; constructor(config: SubschemaConfig) { this.schema = config.schema; diff --git a/packages/delegate/src/Transformer.ts b/packages/delegate/src/Transformer.ts index e637b51e042..d97d7b5ecc9 100644 --- a/packages/delegate/src/Transformer.ts +++ b/packages/delegate/src/Transformer.ts @@ -4,7 +4,6 @@ import { DelegationContext, Transform } from './types'; import { prepareGatewayDocument } from './prepareGatewayDocument'; import { finalizeGatewayRequest } from './finalizeGatewayRequest'; -import { checkResultAndHandleErrors } from './checkResultAndHandleErrors'; interface Transformation { transform: Transform; @@ -48,7 +47,7 @@ export class Transformer> { return finalizeGatewayRequest(request, this.delegationContext); } - public transformResult(originalResult: ExecutionResult) { + public transformResult(originalResult: ExecutionResult): ExecutionResult { let result = originalResult; // from rigth to left @@ -59,6 +58,6 @@ export class Transformer> { } } - return checkResultAndHandleErrors(result, this.delegationContext); + return result; } } diff --git a/packages/delegate/src/checkResultAndHandleErrors.ts b/packages/delegate/src/checkResultAndHandleErrors.ts deleted file mode 100644 index c10964370ba..00000000000 --- a/packages/delegate/src/checkResultAndHandleErrors.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { GraphQLResolveInfo, GraphQLOutputType, GraphQLError, responsePathAsArray, locatedError } from 'graphql'; - -import { AggregateError, getResponseKeyFromInfo, ExecutionResult, relocatedError } from '@graphql-tools/utils'; - -import { DelegationContext } from './types'; -import { resolveExternalValue } from './resolveExternalValue'; - -export function checkResultAndHandleErrors(result: ExecutionResult, delegationContext: DelegationContext): any { - const { - context, - info, - fieldName: responseKey = getResponseKey(info), - subschema, - returnType = getReturnType(info), - skipTypeMerging, - onLocatedError, - } = delegationContext; - - const { data, unpathedErrors } = mergeDataAndErrors( - result.data == null ? undefined : result.data[responseKey], - result.errors == null ? [] : result.errors, - info != null && info.path ? responsePathAsArray(info.path) : undefined, - onLocatedError - ); - - return resolveExternalValue(data, unpathedErrors, subschema, context, info, returnType, skipTypeMerging); -} - -export function mergeDataAndErrors( - data: any, - errors: ReadonlyArray, - path: Array | undefined, - onLocatedError?: (originalError: GraphQLError) => GraphQLError, - index = 1 -): { data: any; unpathedErrors: Array } { - if (data == null) { - if (!errors.length) { - return { data: null, unpathedErrors: [] }; - } - - if (errors.length === 1) { - const error = onLocatedError ? onLocatedError(errors[0]) : errors[0]; - const newPath = - path === undefined ? error.path : error.path === undefined ? path : path.concat(error.path.slice(1)); - - return { data: relocatedError(errors[0], newPath), unpathedErrors: [] }; - } - - // We cast path as any for GraphQL.js 14 compat - // locatedError path argument must be defined, but it is just forwarded to a constructor that allows a undefined value - // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/locatedError.js#L25 - // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/GraphQLError.js#L19 - const newError = locatedError(new AggregateError(errors), undefined as any, path as any); - - return { data: newError, unpathedErrors: [] }; - } - - if (!errors.length) { - return { data, unpathedErrors: [] }; - } - - const unpathedErrors: Array = []; - - const errorMap = new Map>(); - for (const error of errors) { - const pathSegment = error.path?.[index]; - if (pathSegment != null) { - let pathSegmentErrors = errorMap.get(pathSegment); - if (pathSegmentErrors === undefined) { - pathSegmentErrors = [error]; - errorMap.set(pathSegment, pathSegmentErrors); - } else { - pathSegmentErrors.push(error); - } - } else { - unpathedErrors.push(error); - } - } - - for (const [pathSegment, pathSegmentErrors] of errorMap) { - if (data[pathSegment] !== undefined) { - const { data: newData, unpathedErrors: newErrors } = mergeDataAndErrors( - data[pathSegment], - pathSegmentErrors, - path, - onLocatedError, - index + 1 - ); - data[pathSegment] = newData; - unpathedErrors.push(...newErrors); - } else { - unpathedErrors.push(...pathSegmentErrors); - } - } - - return { data, unpathedErrors }; -} - -function getResponseKey(info: GraphQLResolveInfo | undefined): string { - if (info == null) { - throw new Error(`Data cannot be extracted from result without an explicit key or source schema.`); - } - return getResponseKeyFromInfo(info); -} - -function getReturnType(info: GraphQLResolveInfo | undefined): GraphQLOutputType { - if (info == null) { - throw new Error(`Return type cannot be inferred without a source schema.`); - } - return info.returnType; -} diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index d80998d7ead..274fc3624fa 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -1,38 +1,67 @@ -import { defaultFieldResolver, GraphQLResolveInfo } from 'graphql'; +import { GraphQLResolveInfo, defaultFieldResolver } from 'graphql'; import { getResponseKeyFromInfo } from '@graphql-tools/utils'; -import { resolveExternalValue } from './resolveExternalValue'; -import { getSubschema, getUnpathedErrors, isExternalObject } from './mergeFields'; import { ExternalObject } from './types'; +import { createExternalValue } from './externalValues'; +import { + getInitialPath, + getInitialPossibleFields, + getSubschema, + getUnpathedErrors, + isExternalObject, +} from './externalObjects'; + +import { getMergedParent } from './getMergedParent'; + /** * Resolver that knows how to: * a) handle aliases for proxied schemas * b) handle errors from proxied schemas - * c) handle external to internal enum conversion + * c) handle external to internal enum/scalar conversion + * d) handle type merging + * e) handle deferred values */ export function defaultMergedResolver( parent: ExternalObject, args: Record, context: Record, info: GraphQLResolveInfo -) { - if (!parent) { - return null; +): any { + if (!isExternalObject(parent)) { + return defaultFieldResolver(parent, args, context, info); } const responseKey = getResponseKeyFromInfo(info); - // check to see if parent is not a proxied result, i.e. if parent resolver was manually overwritten - // See https://github.com/ardatan/graphql-tools/issues/967 - if (!isExternalObject(parent)) { - return defaultFieldResolver(parent, args, context, info); + const initialPossibleFields = getInitialPossibleFields(parent); + + if (initialPossibleFields === undefined) { + // TODO: can this be removed in the next major release? + // legacy use of delegation without setting transformedSchema + const data = parent[responseKey]; + if (data !== undefined) { + return resolveField(parent, responseKey, context, info); + } + } else if (info.fieldName in initialPossibleFields) { + return resolveField(parent, responseKey, context, info); } - const data = parent[responseKey]; - const unpathedErrors = getUnpathedErrors(parent); + return getMergedParent(parent, context, info).then(mergedParent => + resolveField(mergedParent, responseKey, context, info) + ); +} + +function resolveField( + parent: ExternalObject, + responseKey: string, + context: Record, + info: GraphQLResolveInfo +): any { + const initialPath = getInitialPath(parent); const subschema = getSubschema(parent, responseKey); + const unpathedErrors = getUnpathedErrors(parent); - return resolveExternalValue(data, unpathedErrors, subschema, context, info); + return createExternalValue(parent[responseKey], unpathedErrors, initialPath, subschema, context, info); } diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 55a7c446732..0bbfd0cbbe4 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -10,6 +10,8 @@ import { DocumentNode, GraphQLOutputType, ExecutionArgs, + GraphQLError, + isSchema, } from 'graphql'; import { ValueOrPromise } from 'value-or-promise'; @@ -35,10 +37,10 @@ import { SubschemaConfig, } from './types'; -import { isSubschemaConfig } from './subschemaConfig'; import { Subschema } from './Subschema'; import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; import { Transformer } from './Transformer'; +import { externalValueFromResult } from './externalValues'; export function delegateToSchema, TArgs = any>( options: IDelegateToSchemaOptions @@ -46,7 +48,7 @@ export function delegateToSchema, TArgs = any>( const { info, schema, - rootValue, + rootValue = (schema as SubschemaConfig).rootValue, operationName, operation = getDelegatingOperation(info.parentType, info.schema), fieldName = info.fieldName, @@ -61,7 +63,7 @@ export function delegateToSchema, TArgs = any>( fieldName, selectionSet, fieldNodes, - rootValue: rootValue ?? (schema as SubschemaConfig).rootValue, + rootValue, operationName, context, }); @@ -69,6 +71,8 @@ export function delegateToSchema, TArgs = any>( return delegateRequest({ ...options, request, + operation, + fieldName, }); } @@ -84,13 +88,43 @@ function getDelegationReturnType( export function delegateRequest, TArgs = any>( options: IDelegateRequestOptions ) { - const delegationContext = getDelegationContext(options); + let operationDefinition: Maybe; + let targetFieldName: string; + + const { document, operationName } = options.request; + if (options.fieldName == null) { + operationDefinition = getOperationAST(document, operationName); + if (operationDefinition == null) { + throw new Error('Cannot infer main operation from the provided document.'); + } + targetFieldName = (operationDefinition?.selectionSet.selections[0] as unknown as FieldDefinitionNode).name.value; + } else { + targetFieldName = options.fieldName; + } + + const { + schema, + info, + operation = getDelegatingOperation(info.parentType, info.schema), + returnType = info?.returnType ?? + getDelegationReturnType(schema instanceof GraphQLSchema ? schema : schema.schema, operation, targetFieldName), + context, + onLocatedError = (error: GraphQLError) => error, + validateRequest: shouldValidateRequest, + } = options; + + const delegationContext = getDelegationContext({ + ...options, + operation, + fieldName: targetFieldName, + returnType, + }); const transformer = new Transformer(delegationContext); const processedRequest = transformer.transformRequest(options.request); - if (options.validateRequest) { + if (shouldValidateRequest) { validateRequest(delegationContext, processedRequest.document); } @@ -100,14 +134,32 @@ export function delegateRequest, TArgs = any>( .then(originalResult => { if (isAsyncIterable(originalResult)) { // "subscribe" to the subscription result and map the result through the transforms - return mapAsyncIterator(originalResult, result => transformer.transformResult(result)); + return mapAsyncIterator(originalResult, result => + externalValueFromResult({ + result: transformer.transformResult(result), + schema, + info, + context, + fieldName: targetFieldName, + returnType, + onLocatedError, + }) + ); } - return transformer.transformResult(originalResult); + return externalValueFromResult({ + result: transformer.transformResult(originalResult), + schema, + info, + context, + fieldName: targetFieldName, + returnType, + onLocatedError, + }); }) .resolve(); } -function getDelegationContext({ +export function getDelegationContext({ request, schema, fieldName, @@ -116,7 +168,6 @@ function getDelegationContext({ info, transforms = [], transformedSchema, - skipTypeMerging = false, }: IDelegateRequestOptions): DelegationContext { const { operationType: operation, context, operationName, document } = request; let operationDefinition: Maybe; @@ -137,7 +188,7 @@ function getDelegationContext({ const subschemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig = stitchingInfo?.subschemaMap.get(schema) ?? schema; - if (isSubschemaConfig(subschemaOrSubschemaConfig)) { + if (!isSchema(subschemaOrSubschemaConfig)) { const targetSchema = subschemaOrSubschemaConfig.schema; return { subschema: schema, @@ -156,7 +207,6 @@ function getDelegationContext({ transformedSchema: transformedSchema ?? (subschemaOrSubschemaConfig instanceof Subschema ? subschemaOrSubschemaConfig.transformedSchema : targetSchema), - skipTypeMerging, }; } @@ -173,11 +223,10 @@ function getDelegationContext({ returnType ?? info?.returnType ?? getDelegationReturnType(subschemaOrSubschemaConfig, operation, targetFieldName), transforms, transformedSchema: transformedSchema ?? subschemaOrSubschemaConfig, - skipTypeMerging, }; } -function validateRequest(delegationContext: DelegationContext, document: DocumentNode) { +export function validateRequest(delegationContext: DelegationContext, document: DocumentNode) { const errors = validate(delegationContext.targetSchema, document); if (errors.length > 0) { if (errors.length > 1) { @@ -189,7 +238,7 @@ function validateRequest(delegationContext: DelegationContext, document: Do } } -function getExecutor(delegationContext: DelegationContext): Executor { +export function getExecutor(delegationContext: DelegationContext): Executor { const { subschemaConfig, targetSchema, context } = delegationContext; let executor: Executor = subschemaConfig?.executor || createDefaultExecutor(targetSchema); diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts new file mode 100644 index 00000000000..1803f206d77 --- /dev/null +++ b/packages/delegate/src/externalObjects.ts @@ -0,0 +1,70 @@ +import { GraphQLError, GraphQLFieldMap, GraphQLObjectType, GraphQLResolveInfo, GraphQLSchema } from 'graphql'; + +import { + OBJECT_SUBSCHEMA_SYMBOL, + INITIAL_POSSIBLE_FIELDS, + INFO_SYMBOL, + RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL, + UNPATHED_ERRORS_SYMBOL, + INITIAL_PATH_SYMBOL, +} from './symbols'; +import { ExternalObject, SubschemaConfig } from './types'; +import { Subschema } from './Subschema'; + +export function isExternalObject(data: any): data is ExternalObject { + return data?.[UNPATHED_ERRORS_SYMBOL] !== undefined; +} + +export function createExternalObject>( + object: any, + errors: Array, + initialPath: Array, + subschema: GraphQLSchema | SubschemaConfig, + info?: GraphQLResolveInfo +): ExternalObject { + const schema = + subschema instanceof Subschema ? subschema.transformedSchema : (subschema as SubschemaConfig)?.schema ?? subschema; + + const initialPossibleFields = (schema.getType(object.__typename) as GraphQLObjectType)?.getFields(); + + const newObject = { ...object }; + + Object.defineProperties(newObject, { + [INITIAL_PATH_SYMBOL]: { value: initialPath }, + [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, + [INITIAL_POSSIBLE_FIELDS]: { value: initialPossibleFields }, + [INFO_SYMBOL]: { value: info }, + [RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL]: { value: Object.create(null) }, + [UNPATHED_ERRORS_SYMBOL]: { value: errors }, + }); + + return newObject; +} + +export function getInitialPath(object: ExternalObject): Array { + return object[INITIAL_PATH_SYMBOL]; +} + +export function getInitialPossibleFields(object: ExternalObject): GraphQLFieldMap { + return object[INITIAL_POSSIBLE_FIELDS]; +} + +export function getInfo(object: ExternalObject): GraphQLResolveInfo { + return object[INFO_SYMBOL]; +} + +export function getUnpathedErrors(object: ExternalObject): Array { + return object[UNPATHED_ERRORS_SYMBOL]; +} + +export function getObjectSubchema(object: ExternalObject): GraphQLSchema | SubschemaConfig { + return object[OBJECT_SUBSCHEMA_SYMBOL]; +} + +export function getSubschemaMap(object: ExternalObject): Record { + return object[RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL]; +} + +export function getSubschema(object: ExternalObject, responseKey: string): GraphQLSchema | SubschemaConfig { + return object[RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? object[OBJECT_SUBSCHEMA_SYMBOL]; +} diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts new file mode 100644 index 00000000000..7af48cda0da --- /dev/null +++ b/packages/delegate/src/externalValues.ts @@ -0,0 +1,164 @@ +import { + GraphQLResolveInfo, + getNullableType, + isCompositeType, + isListType, + GraphQLError, + GraphQLSchema, + GraphQLList, + GraphQLType, + locatedError, + GraphQLOutputType, + responsePathAsArray, +} from 'graphql'; + +import { AggregateError, relocatedError } from '@graphql-tools/utils'; + +import { ExternalValueFromResultOptions, SubschemaConfig } from './types'; +import { createExternalObject } from './externalObjects'; +import { mergeDataAndErrors } from './mergeDataAndErrors'; + +export function externalValueFromResult>({ + result, + schema, + info, + context, + fieldName = getFieldName(info), + returnType = getReturnType(info), + onLocatedError = (error: GraphQLError) => error, +}: ExternalValueFromResultOptions): any { + const data = result.data?.[fieldName]; + const errors = result.errors ?? []; + const initialPath = info ? responsePathAsArray(info.path) : []; + + const { data: newData, unpathedErrors } = mergeDataAndErrors(data, errors, onLocatedError); + + return createExternalValue(newData, unpathedErrors, initialPath, schema, context, info, returnType); +} + +export function createExternalValue>( + data: any, + unpathedErrors: Array, + initialPath: Array, + subschema: GraphQLSchema | SubschemaConfig, + context?: TContext, + info?: GraphQLResolveInfo, + returnType = getReturnType(info) +): any { + const type = getNullableType(returnType); + + if (data instanceof GraphQLError) { + return relocatedError(data, data.path ? initialPath.concat(data.path) : initialPath); + } + + if (data instanceof Error) { + return data; + } + + if (data == null) { + return reportUnpathedErrorsViaNull(unpathedErrors); + } + + if ('parseValue' in type) { + return type.parseValue(data); + } else if (isCompositeType(type)) { + return createExternalObject(data, unpathedErrors, initialPath, subschema, info); + } else if (isListType(type)) { + return createExternalList(type, data, unpathedErrors, initialPath, subschema, context, info); + } +} + +function createExternalList>( + type: GraphQLList, + list: Array, + unpathedErrors: Array, + initialPath: Array, + subschema: GraphQLSchema | SubschemaConfig, + context?: Record, + info?: GraphQLResolveInfo +) { + return list.map(listMember => + createExternalListMember( + getNullableType(type.ofType), + listMember, + unpathedErrors, + initialPath, + subschema, + context, + info + ) + ); +} + +function createExternalListMember>( + type: GraphQLType, + listMember: any, + unpathedErrors: Array, + initialPath: Array, + subschema: GraphQLSchema | SubschemaConfig, + context?: Record, + info?: GraphQLResolveInfo +): any { + if (listMember instanceof GraphQLError) { + return relocatedError(listMember, listMember.path ? initialPath.concat(listMember.path) : initialPath); + } + + if (listMember instanceof Error) { + return listMember; + } + + if (listMember == null) { + return reportUnpathedErrorsViaNull(unpathedErrors); + } + + if ('parseValue' in type) { + return type.parseValue(listMember); + } else if (isCompositeType(type)) { + return createExternalObject(listMember, unpathedErrors, initialPath, subschema, info); + } else if (isListType(type)) { + return createExternalList(type, listMember, unpathedErrors, initialPath, subschema, context, info); + } +} + +const reportedErrors = new WeakMap(); + +function reportUnpathedErrorsViaNull(unpathedErrors: Array) { + if (unpathedErrors.length) { + const unreportedErrors: Array = []; + for (const error of unpathedErrors) { + if (!reportedErrors.has(error)) { + unreportedErrors.push(error); + reportedErrors.set(error, true); + } + } + + if (unreportedErrors.length) { + if (unreportedErrors.length === 1) { + return unreportedErrors[0]; + } + + const combinedError = new AggregateError(unreportedErrors); + // We cast path as any for GraphQL.js 14 compat + // locatedError path argument must be defined, but it is just forwarded to a constructor that allows a undefined value + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/locatedError.js#L25 + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/GraphQLError.js#L19 + return locatedError(combinedError, undefined as any, unreportedErrors[0].path as any); + } + } + + return null; +} + +function getFieldName(info: GraphQLResolveInfo | undefined): string { + if (info == null) { + throw new Error(`Data cannot be extracted from result without an explicit key or source schema.`); + } + return info.fieldName; +} + +function getReturnType(info: GraphQLResolveInfo | undefined): GraphQLOutputType { + if (info == null) { + throw new Error(`Return type cannot be inferred without a source schema.`); + } + return info.returnType; +} diff --git a/packages/delegate/src/getMergedParent.ts b/packages/delegate/src/getMergedParent.ts new file mode 100644 index 00000000000..03e547740bb --- /dev/null +++ b/packages/delegate/src/getMergedParent.ts @@ -0,0 +1,555 @@ +import { + FieldNode, + Kind, + GraphQLResolveInfo, + SelectionSetNode, + GraphQLObjectType, + getNamedType, + responsePathAsArray, + GraphQLError, + locatedError, + GraphQLSchema, +} from 'graphql'; + +import DataLoader from 'dataloader'; + +import { + Maybe, + collectFields, + relocatedError, + memoize1, + memoize1of2, + memoize2, + memoize3, + memoize4ofMany, +} from '@graphql-tools/utils'; + +import { ExternalObject, MergedTypeInfo, StitchingInfo } from './types'; +import { Subschema } from './Subschema'; +import { + getInfo, + getInitialPath, + getObjectSubchema, + getSubschemaMap, + getUnpathedErrors, + isExternalObject, +} from './externalObjects'; + +const getMergeDetails = memoize1of2(function getMergeDetails( + info: GraphQLResolveInfo, + parent: ExternalObject +): + | { + stitchingInfo: StitchingInfo; + mergedTypeInfo: MergedTypeInfo; + sourceSubschema: Subschema; + targetSubschemas: Array; + } + | undefined { + const schema = info.schema; + const stitchingInfo: Maybe = schema.extensions?.['stitchingInfo']; + if (stitchingInfo == null) { + return; + } + + const parentTypeName = info.parentType.name; + const mergedTypeInfo = stitchingInfo.mergedTypes[parentTypeName]; + if (mergedTypeInfo === undefined) { + return; + } + + // In the stitching context, all subschemas are compiled Subschema objects rather than SubschemaConfig objects + const sourceSubschema = getObjectSubchema(parent) as Subschema; + const targetSubschemas = mergedTypeInfo.targetSubschemas.get(sourceSubschema); + if (targetSubschemas === undefined || targetSubschemas.length === 0) { + return; + } + + return { + stitchingInfo, + mergedTypeInfo, + sourceSubschema, + targetSubschemas, + }; +}); + +const loaders: WeakMap>> = new WeakMap(); + +export async function getMergedParent( + parent: ExternalObject, + context: Record, + info: GraphQLResolveInfo +): Promise { + let loader = loaders.get(parent); + if (loader === undefined) { + loader = new DataLoader(infos => getMergedParentsFromInfos(parent, context, infos)); + loaders.set(parent, loader); + } + return loader.load(info); +} + +async function getMergedParentsFromInfos( + parent: ExternalObject, + context: Record, + infos: ReadonlyArray +): Promise>> { + const mergeDetails = getMergeDetails(infos[0], parent); + if (!mergeDetails) { + return Array(infos.length).fill(parent); + } + + const { mergedTypeInfo, sourceSubschema } = mergeDetails; + + const { requiredKeys, delegationMaps, stageMap } = buildDelegationPlan( + mergedTypeInfo, + infos[0].schema, + sourceSubschema, + ...infos.map(info => info.fieldNodes) + ); + + if (!delegationMaps.length) { + return Array(infos.length).fill(parent); + } + + const promises: Array> = []; + const parentInfo = getInfo(parent); + const schema = parentInfo.schema; + const type = schema.getType(parent.__typename) as GraphQLObjectType; + const parentPath = responsePathAsArray(parentInfo.path); + let promise = executeDelegationStage( + delegationMaps[0], + schema, + type, + mergedTypeInfo, + context, + parent, + parentInfo, + parentPath + ).then(() => parent); + promises.push(promise); + for (let i = 1, delegationStage = delegationMaps[i]; i < delegationMaps.length; i++) { + promise = promise.then(parent => + executeDelegationStage( + delegationStage, + schema, + type, + mergedTypeInfo, + context, + parent, + parentInfo, + parentPath + ).then(() => parent) + ); + promises.push(promise); + } + + return infos.map(info => { + const keys = requiredKeys[info.fieldName]; + if (keys === undefined) { + const delegationStage = stageMap[info.fieldName]; + if (delegationStage !== undefined) { + return promises[delegationStage].then(() => parent); + } + return Promise.resolve(parent); + } + + return Promise.all( + Array.from(keys.values()).map(fieldName => { + const delegationStage = stageMap[fieldName]; + if (delegationStage !== undefined) { + return promises[delegationStage].then(() => parent); + } + return Promise.resolve(parent); + }) + ).then(() => parent); + }); +} + +function sortSubschemasByProxiability( + mergedTypeInfo: MergedTypeInfo, + sourceSubschemas: Array, + targetSubschemas: Array, + fieldNodes: Array +): { + proxiableSubschemas: Array; + nonProxiableSubschemas: Array; +} { + // 1. calculate if possible to delegate to given subschema + + const proxiableSubschemas: Array = []; + const nonProxiableSubschemas: Array = []; + + for (const t of targetSubschemas) { + const selectionSet = mergedTypeInfo.selectionSets.get(t); + const fieldSelectionSets = mergedTypeInfo.fieldSelectionSets.get(t); + if (selectionSet != null && !subschemaTypesContainSelectionSet(mergedTypeInfo, sourceSubschemas, selectionSet)) { + nonProxiableSubschemas.push(t); + } else { + if ( + fieldSelectionSets == null || + fieldNodes.every(fieldNode => { + const fieldName = fieldNode.name.value; + const fieldSelectionSet = fieldSelectionSets[fieldName]; + return ( + fieldSelectionSet == null || + subschemaTypesContainSelectionSet(mergedTypeInfo, sourceSubschemas, fieldSelectionSet) + ); + }) + ) { + proxiableSubschemas.push(t); + } else { + nonProxiableSubschemas.push(t); + } + } + } + + return { + proxiableSubschemas, + nonProxiableSubschemas, + }; +} + +function calculateDelegationStage( + mergedTypeInfo: MergedTypeInfo, + sourceSubschemas: Array, + targetSubschemas: Array, + fieldNodes: Array +): { + proxiableSubschemas: Array; + nonProxiableSubschemas: Array; + delegationMap: Map>; + proxiableFieldNodes: Array; + unproxiableFieldNodes: Array; +} { + const { proxiableSubschemas, nonProxiableSubschemas } = sortSubschemasByProxiability( + mergedTypeInfo, + sourceSubschemas, + targetSubschemas, + fieldNodes + ); + + const { delegationMap, proxiableFieldNodes, unproxiableFieldNodes } = buildDelegationStage( + mergedTypeInfo, + fieldNodes, + proxiableSubschemas + ); + + return { + proxiableSubschemas, + nonProxiableSubschemas, + delegationMap, + proxiableFieldNodes, + unproxiableFieldNodes, + }; +} + +export const buildDelegationPlan = memoize4ofMany(function buildDelegationPlan( + mergedTypeInfo: MergedTypeInfo, + schema: GraphQLSchema, + sourceSubschema: Subschema, + ...fieldNodeArrays: Array> +): { + requiredKeys: Record>; + delegationMaps: Array>>; + stageMap: Record; +} { + const requiredKeys: Record> = Object.create(null); + const delegationMaps: Array>> = []; + const stageMap = Object.create(null); + + const stitchingInfo: Maybe = schema.extensions?.['stitchingInfo']; + if (stitchingInfo == null) { + return { requiredKeys, delegationMaps, stageMap }; + } + + const targetSubschemas = mergedTypeInfo.targetSubschemas.get(sourceSubschema); + if (targetSubschemas === undefined || targetSubschemas.length === 0) { + return { requiredKeys, delegationMaps, stageMap }; + } + + const parentTypeName = mergedTypeInfo.typeName; + const sourceSubschemaParentType = sourceSubschema.transformedSchema.getType(parentTypeName) as GraphQLObjectType; + const sourceSubschemaFields = sourceSubschemaParentType.getFields(); + const subschemaFields = mergedTypeInfo.subschemaFields; + const typeFieldNodes = stitchingInfo.fieldNodesByField?.[parentTypeName]; + + const fieldNodeSet = new Set(); + + const fieldNodesByType = stitchingInfo.fieldNodesByType?.[parentTypeName]; + if (fieldNodesByType !== undefined) { + for (const fieldNode of fieldNodesByType) { + const fieldName = fieldNode.name.value; + if (!sourceSubschemaFields[fieldName]) { + const fieldName = fieldNode.name.value; + if (!sourceSubschemaFields[fieldName]) { + fieldNodeSet.add(fieldNode); + } + } + } + } + + for (const fieldNodeArray of fieldNodeArrays) { + const fieldName = fieldNodeArray[0].name.value; + if (subschemaFields[fieldName] !== undefined) { + // merged subschema field + for (const fieldNode of fieldNodeArray) { + const fieldName = fieldNode.name.value; + if (!sourceSubschemaFields[fieldName]) { + fieldNodeSet.add(fieldNode); + } + } + } else { + // gateway field + if (requiredKeys[fieldName] === undefined) { + requiredKeys[fieldName] = new Set(); + } + } + + const keyFieldNodes = new Set(); + + const fieldNodesByField = typeFieldNodes?.[fieldName]; + if (fieldNodesByField !== undefined) { + for (const fieldNode of fieldNodesByField) { + keyFieldNodes.add(fieldNode); + } + } + + if (requiredKeys[fieldName] !== undefined) { + for (const fieldNode of keyFieldNodes.values()) { + requiredKeys[fieldName].add(fieldNode.name.value); + } + } + + for (const fieldNode of keyFieldNodes) { + fieldNodeSet.add(fieldNode); + } + } + + const fieldNodes = Array.from(fieldNodeSet); + + let sourceSubschemas = createSubschemas(sourceSubschema); + let delegationStage = calculateDelegationStage(mergedTypeInfo, sourceSubschemas, targetSubschemas, fieldNodes); + + let stageIndex = 0; + let delegationMap = delegationStage.delegationMap; + while (delegationMap.size) { + delegationMaps.push(delegationMap); + + const { proxiableSubschemas, nonProxiableSubschemas, proxiableFieldNodes, unproxiableFieldNodes } = delegationStage; + + sourceSubschemas = combineSubschemas(sourceSubschemas, proxiableSubschemas); + + for (const fieldNode of proxiableFieldNodes) { + stageMap[fieldNode.name.value] = stageIndex; + } + stageIndex++; + + delegationStage = calculateDelegationStage( + mergedTypeInfo, + sourceSubschemas, + nonProxiableSubschemas, + unproxiableFieldNodes + ); + + delegationMap = delegationStage.delegationMap; + } + + return { + requiredKeys, + delegationMaps, + stageMap, + }; +}); + +function buildDelegationStage( + mergedTypeInfo: MergedTypeInfo, + fieldNodes: Array, + proxiableSubschemas: Array +): { + delegationMap: Map>; + proxiableFieldNodes: Array; + unproxiableFieldNodes: Array; +} { + const { uniqueFields, nonUniqueFields } = mergedTypeInfo; + const proxiableFieldNodes: Array = []; + const unproxiableFieldNodes: Array = []; + + // 2. for each selection: + + const delegationMap: Map> = new Map(); + for (const fieldNode of fieldNodes) { + if (fieldNode.name.value === '__typename') { + continue; + } + + // 2a. use uniqueFields map to assign fields to subschema if one of possible subschemas + + const uniqueSubschema: Subschema = uniqueFields[fieldNode.name.value]; + if (uniqueSubschema != null) { + if (!proxiableSubschemas.includes(uniqueSubschema)) { + unproxiableFieldNodes.push(fieldNode); + continue; + } + + const existingSubschema = delegationMap.get(uniqueSubschema); + if (existingSubschema != null) { + existingSubschema.push(fieldNode); + } else { + delegationMap.set(uniqueSubschema, [fieldNode]); + } + proxiableFieldNodes.push(fieldNode); + + continue; + } + + // 2b. use nonUniqueFields to assign to a possible subschema, + // preferring one of the subschemas already targets of delegation + + let nonUniqueSubschemas: Array = nonUniqueFields[fieldNode.name.value]; + if (nonUniqueSubschemas == null) { + unproxiableFieldNodes.push(fieldNode); + continue; + } + + nonUniqueSubschemas = nonUniqueSubschemas.filter(s => proxiableSubschemas.includes(s)); + if (!nonUniqueSubschemas.length) { + unproxiableFieldNodes.push(fieldNode); + continue; + } + + const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); + if (existingSubschema != null) { + // It is okay we previously explicitly check whether the map has the element. + (delegationMap.get(existingSubschema)! as Array).push(fieldNode); + } else { + delegationMap.set(nonUniqueSubschemas[0], [fieldNode]); + } + proxiableFieldNodes.push(fieldNode); + } + + return { + delegationMap, + proxiableFieldNodes, + unproxiableFieldNodes, + }; +} + +const createSubschemas = memoize1(function createSubschemas(subschema: Subschema): Array { + return [subschema]; +}); + +const combineSubschemas = memoize2(function combineSubschemas( + subschemas: Array, + additionalSubschemas: Array +): Array { + return subschemas.concat(additionalSubschemas); +}); + +async function executeDelegationStage( + delegationMap: Map>, + schema: GraphQLSchema, + type: GraphQLObjectType, + mergedTypeInfo: MergedTypeInfo, + context: Record, + object: ExternalObject, + parentInfo: GraphQLResolveInfo, + parentPath: Array +): Promise { + const initialPath = getInitialPath(object); + const newSubschemaMap = getSubschemaMap(object); + + const unpathedErrors = getUnpathedErrors(object); + + await Promise.all( + [...delegationMap.entries()].map(async ([s, fieldNodes]) => { + const resolver = mergedTypeInfo.resolvers.get(s); + if (resolver) { + const selectionSet: SelectionSetNode = { kind: Kind.SELECTION_SET, selections: fieldNodes }; + let source: unknown; + try { + source = await resolver(object, context, parentInfo, s, selectionSet); + } catch (error) { + source = error; + } + + if (source instanceof Error || source === null) { + const fieldNodes = collectFields(schema, {}, {}, type, selectionSet, new Map(), new Set()); + + const nullResult = Object.create(null); + if (source instanceof GraphQLError && source.path) { + const basePath = parentPath.slice(initialPath.length); + for (const [responseKey] of fieldNodes) { + const tailPath = + source.path.length === parentPath.length ? [responseKey] : source.path.slice(initialPath.length); + const newPath = basePath.concat(tailPath); + nullResult[responseKey] = relocatedError(source, newPath); + } + } else if (source instanceof Error) { + const basePath = parentPath.slice(initialPath.length); + for (const [responseKey] of fieldNodes) { + const newPath = basePath.concat([responseKey]); + nullResult[responseKey] = locatedError(source, fieldNodes[responseKey], newPath); + } + } else { + for (const [responseKey] of fieldNodes) { + nullResult[responseKey] = null; + } + } + source = nullResult; + } + + // TODO: fold this assign into loop below? + Object.assign(object, source); + + // TODO: is this check necessary or just to make TS happy? + if (!isExternalObject(source)) { + return; + } + + const objectSubschema = getObjectSubchema(source); + const subschemaMap = getSubschemaMap(source); + for (const responseKey in source) { + newSubschemaMap[responseKey] = subschemaMap?.[responseKey] ?? objectSubschema; + } + unpathedErrors.push(...getUnpathedErrors(source)); + } + }) + ); +} + +const subschemaTypesContainSelectionSet = memoize3(function subschemaTypesContainSelectionSet( + mergedTypeInfo: MergedTypeInfo, + sourceSubschemas: Array, + selectionSet: SelectionSetNode +) { + return typesContainSelectionSet( + sourceSubschemas.map( + sourceSubschema => sourceSubschema.transformedSchema.getType(mergedTypeInfo.typeName) as GraphQLObjectType + ), + selectionSet + ); +}); + +function typesContainSelectionSet(types: Array, selectionSet: SelectionSetNode): boolean { + const fieldMaps = types.map(type => type.getFields()); + + for (const selection of selectionSet.selections) { + if (selection.kind === Kind.FIELD) { + const fields = fieldMaps.map(fieldMap => fieldMap[selection.name.value]).filter(field => field != null); + if (!fields.length) { + return false; + } + + if (selection.selectionSet != null) { + return typesContainSelectionSet( + fields.map(field => getNamedType(field.type)) as Array, + selection.selectionSet + ); + } + } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.typeCondition?.name.value === types[0].name) { + return typesContainSelectionSet(types, selection.selectionSet); + } + } + + return true; +} diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index e8f39de2bbc..cc55889fa26 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -4,7 +4,8 @@ export * from './applySchemaTransforms'; export * from './createRequest'; export * from './defaultMergedResolver'; export * from './delegateToSchema'; -export * from './mergeFields'; -export * from './resolveExternalValue'; +export * from './getMergedParent'; +export * from './externalObjects'; +export * from './externalValues'; export * from './subschemaConfig'; export * from './types'; diff --git a/packages/delegate/src/mergeDataAndErrors.ts b/packages/delegate/src/mergeDataAndErrors.ts new file mode 100644 index 00000000000..01abb9333f9 --- /dev/null +++ b/packages/delegate/src/mergeDataAndErrors.ts @@ -0,0 +1,73 @@ +import { GraphQLError, locatedError } from 'graphql'; + +import { AggregateError, relocatedError } from '@graphql-tools/utils'; + +export function mergeDataAndErrors( + data: any, + errors: ReadonlyArray = [], + onLocatedError = (originalError: GraphQLError) => originalError, + index = 1 +): { data: any; unpathedErrors: Array } { + if (data == null) { + if (!errors.length) { + return { data: null, unpathedErrors: [] }; + } + + if (errors.length === 1) { + const error = onLocatedError(errors[0]); + const newPath = error.path === undefined ? [] : error.path.slice(1); + const newError = relocatedError(error, newPath); + return { data: newError, unpathedErrors: [] }; + } + + const newErrors = errors.map(error => onLocatedError(error)); + const firstError = newErrors[0]; + const newPath = firstError.path === undefined ? [] : firstError.path.slice(1); + // We cast path as any for GraphQL.js 14 compat + // locatedError path argument must be defined, but it is just forwarded to a constructor that allows a undefined value + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/locatedError.js#L25 + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/GraphQLError.js#L19 + const newError = locatedError(new AggregateError(newErrors), undefined as any, newPath); + + return { data: newError, unpathedErrors: [] }; + } + + if (!errors.length) { + return { data, unpathedErrors: [] }; + } + + const unpathedErrors: Array = []; + + const errorMap = new Map>(); + for (const error of errors) { + const pathSegment = error.path?.[index]; + if (pathSegment != null) { + let pathSegmentErrors = errorMap.get(pathSegment); + if (pathSegmentErrors === undefined) { + pathSegmentErrors = [error]; + errorMap.set(pathSegment, pathSegmentErrors); + } else { + pathSegmentErrors.push(error); + } + } else { + unpathedErrors.push(error); + } + } + + for (const [pathSegment, pathSegmentErrors] of errorMap) { + if (data[pathSegment] !== undefined) { + const { data: newData, unpathedErrors: newErrors } = mergeDataAndErrors( + data[pathSegment], + pathSegmentErrors, + onLocatedError, + index + 1 + ); + data[pathSegment] = newData; + unpathedErrors.push(...newErrors); + } else { + unpathedErrors.push(...pathSegmentErrors); + } + } + + return { data, unpathedErrors }; +} diff --git a/packages/delegate/src/mergeFields.ts b/packages/delegate/src/mergeFields.ts deleted file mode 100644 index a679f016f1e..00000000000 --- a/packages/delegate/src/mergeFields.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - GraphQLResolveInfo, - SelectionSetNode, - GraphQLObjectType, - responsePathAsArray, - GraphQLError, - locatedError, - GraphQLSchema, - FieldNode, -} from 'graphql'; - -import { collectFields, relocatedError } from '@graphql-tools/utils'; - -import { ExternalObject, MergedTypeInfo, SubschemaConfig } from './types'; -import { FIELD_SUBSCHEMA_MAP_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; -import { Subschema } from './Subschema'; - -export function isExternalObject(data: any): data is ExternalObject { - return data[UNPATHED_ERRORS_SYMBOL] !== undefined; -} - -export function annotateExternalObject( - object: any, - errors: Array, - subschema: GraphQLSchema | SubschemaConfig | undefined, - subschemaMap: Record>> -): ExternalObject { - Object.defineProperties(object, { - [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, - [FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: subschemaMap }, - [UNPATHED_ERRORS_SYMBOL]: { value: errors }, - }); - return object; -} - -export function getSubschema(object: ExternalObject, responseKey: string): GraphQLSchema | SubschemaConfig { - return object[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? object[OBJECT_SUBSCHEMA_SYMBOL]; -} - -export function getUnpathedErrors(object: ExternalObject): Array { - return object[UNPATHED_ERRORS_SYMBOL]; -} - -const EMPTY_ARRAY: any[] = []; -const EMPTY_OBJECT = Object.create(null); - -export async function mergeFields( - mergedTypeInfo: MergedTypeInfo, - object: any, - sourceSubschema: Subschema, - context: any, - info: GraphQLResolveInfo -): Promise { - const delegationMaps = mergedTypeInfo.delegationPlanBuilder( - info.schema, - sourceSubschema, - info.variableValues != null && Object.keys(info.variableValues).length > 0 ? info.variableValues : EMPTY_OBJECT, - info.fragments != null && Object.keys(info.fragments).length > 0 ? info.fragments : EMPTY_OBJECT, - info.fieldNodes?.length ? (info.fieldNodes as FieldNode[]) : EMPTY_ARRAY - ); - - for (const delegationMap of delegationMaps) { - await executeDelegationStage(mergedTypeInfo, delegationMap, object, context, info); - } - - return object; -} - -async function executeDelegationStage( - mergedTypeInfo: MergedTypeInfo, - delegationMap: Map, - object: ExternalObject, - context: any, - info: GraphQLResolveInfo -): Promise { - const combinedErrors = object[UNPATHED_ERRORS_SYMBOL]; - - const path = responsePathAsArray(info.path); - - const combinedFieldSubschemaMap = object[FIELD_SUBSCHEMA_MAP_SYMBOL]; - - const type = info.schema.getType(object.__typename) as GraphQLObjectType; - - await Promise.all( - [...delegationMap.entries()].map(async ([s, selectionSet]) => { - const resolver = mergedTypeInfo.resolvers.get(s); - if (resolver) { - let source: any; - try { - source = await resolver(object, context, info, s, selectionSet); - } catch (error: any) { - source = error; - } - if (source instanceof Error || source == null) { - const fieldNodeResponseKeyMap = collectFields(info.schema, {}, {}, type, selectionSet, new Map(), new Set()); - const nullResult = {}; - for (const [responseKey, fieldNodes] of fieldNodeResponseKeyMap) { - const combinedPath = [...path, responseKey]; - if (source instanceof GraphQLError) { - nullResult[responseKey] = relocatedError(source, combinedPath); - } else if (source instanceof Error) { - nullResult[responseKey] = locatedError(source, fieldNodes, combinedPath); - } else { - nullResult[responseKey] = null; - } - } - source = nullResult; - } else { - if (source[UNPATHED_ERRORS_SYMBOL]) { - combinedErrors.push(...source[UNPATHED_ERRORS_SYMBOL]); - } - } - - const objectSubschema = source[OBJECT_SUBSCHEMA_SYMBOL]; - const fieldSubschemaMap = source[FIELD_SUBSCHEMA_MAP_SYMBOL]; - for (const responseKey in source) { - object[responseKey] = source[responseKey]; - combinedFieldSubschemaMap[responseKey] = fieldSubschemaMap?.[responseKey] ?? objectSubschema; - } - } - }) - ); -} diff --git a/packages/delegate/src/prepareGatewayDocument.ts b/packages/delegate/src/prepareGatewayDocument.ts index 3601e3d55e5..4c4d1d2ea77 100644 --- a/packages/delegate/src/prepareGatewayDocument.ts +++ b/packages/delegate/src/prepareGatewayDocument.ts @@ -21,8 +21,8 @@ import { } from 'graphql'; import { implementsAbstractType, getRootTypeNames, memoize2 } from '@graphql-tools/utils'; - import { getDocumentMetadata } from './getDocumentMetadata'; + import { StitchingInfo } from './types'; export function prepareGatewayDocument( diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts deleted file mode 100644 index 5139121ad0f..00000000000 --- a/packages/delegate/src/resolveExternalValue.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { - GraphQLResolveInfo, - getNullableType, - isCompositeType, - isListType, - GraphQLError, - GraphQLSchema, - GraphQLCompositeType, - isAbstractType, - GraphQLList, - GraphQLType, - locatedError, - GraphQLOutputType, -} from 'graphql'; - -import { AggregateError, Maybe } from '@graphql-tools/utils'; - -import { StitchingInfo, SubschemaConfig } from './types'; -import { annotateExternalObject, isExternalObject, mergeFields } from './mergeFields'; -import { Subschema } from './Subschema'; - -export function resolveExternalValue( - result: any, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context?: Record, - info?: GraphQLResolveInfo, - returnType = getReturnType(info), - skipTypeMerging?: boolean -): any { - const type = getNullableType(returnType); - - if (result instanceof Error) { - return result; - } - - if (result == null) { - return reportUnpathedErrorsViaNull(unpathedErrors); - } - - if ('parseValue' in type) { - return type.parseValue(result); - } else if (isCompositeType(type)) { - return resolveExternalObject(type, result, unpathedErrors, subschema, context, info, skipTypeMerging); - } else if (isListType(type)) { - return resolveExternalList(type, result, unpathedErrors, subschema, context, info, skipTypeMerging); - } -} - -function resolveExternalObject( - type: GraphQLCompositeType, - object: any, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context?: Record, - info?: GraphQLResolveInfo, - skipTypeMerging?: boolean -) { - // if we have already resolved this object, for example, when the identical object appears twice - // in a list, see https://github.com/ardatan/graphql-tools/issues/2304 - if (!isExternalObject(object)) { - annotateExternalObject(object, unpathedErrors, subschema, Object.create(null)); - } - - if (skipTypeMerging || info == null) { - return object; - } - - const stitchingInfo = info.schema.extensions?.['stitchingInfo'] as Maybe; - - if (stitchingInfo == null) { - return object; - } - - let typeName: string; - - if (isAbstractType(type)) { - const resolvedType = info.schema.getType(object.__typename); - if (resolvedType == null) { - throw new Error( - `Unable to resolve type '${object.__typename}'. Did you forget to include a transform that renames types? Did you delegate to the original subschema rather that the subschema config object containing the transform?` - ); - } - typeName = resolvedType.name; - } else { - typeName = type.name; - } - - const mergedTypeInfo = stitchingInfo.mergedTypes[typeName]; - let targetSubschemas: undefined | Array; - - // Within the stitching context, delegation to a stitched GraphQLSchema or SubschemaConfig - // will be redirected to the appropriate Subschema object, from which merge targets can be queried. - if (mergedTypeInfo != null) { - targetSubschemas = mergedTypeInfo.targetSubschemas.get(subschema as Subschema); - } - - // If there are no merge targets from the subschema, return. - if (!targetSubschemas || !targetSubschemas.length) { - return object; - } - - return mergeFields(mergedTypeInfo, object, subschema as Subschema, context, info); -} - -function resolveExternalList( - type: GraphQLList, - list: Array, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context?: Record, - info?: GraphQLResolveInfo, - skipTypeMerging?: boolean -) { - return list.map(listMember => - resolveExternalListMember( - getNullableType(type.ofType), - listMember, - unpathedErrors, - subschema, - context, - info, - skipTypeMerging - ) - ); -} - -function resolveExternalListMember( - type: GraphQLType, - listMember: any, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context?: Record, - info?: GraphQLResolveInfo, - skipTypeMerging?: boolean -): any { - if (listMember instanceof Error) { - return listMember; - } - - if (listMember == null) { - return reportUnpathedErrorsViaNull(unpathedErrors); - } - - if ('parseValue' in type) { - return type.parseValue(listMember); - } else if (isCompositeType(type)) { - return resolveExternalObject(type, listMember, unpathedErrors, subschema, context, info, skipTypeMerging); - } else if (isListType(type)) { - return resolveExternalList(type, listMember, unpathedErrors, subschema, context, info, skipTypeMerging); - } -} - -const reportedErrors = new WeakMap(); - -function reportUnpathedErrorsViaNull(unpathedErrors: Array) { - if (unpathedErrors.length) { - const unreportedErrors: Array = []; - for (const error of unpathedErrors) { - if (!reportedErrors.has(error)) { - unreportedErrors.push(error); - reportedErrors.set(error, true); - } - } - - if (unreportedErrors.length) { - if (unreportedErrors.length === 1) { - return unreportedErrors[0]; - } - - const combinedError = new AggregateError(unreportedErrors); - // We cast path as any for GraphQL.js 14 compat - // locatedError path argument must be defined, but it is just forwarded to a constructor that allows a undefined value - // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/locatedError.js#L25 - // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/GraphQLError.js#L19 - return locatedError(combinedError, undefined as any, unreportedErrors[0].path as any); - } - } - - return null; -} - -function getReturnType(info: GraphQLResolveInfo | undefined): GraphQLOutputType { - if (info == null) { - throw new Error(`Return type cannot be inferred without a source schema.`); - } - return info.returnType; -} diff --git a/packages/delegate/src/symbols.ts b/packages/delegate/src/symbols.ts index dad8b7b958a..5fea6e5418b 100644 --- a/packages/delegate/src/symbols.ts +++ b/packages/delegate/src/symbols.ts @@ -1,3 +1,7 @@ -export const UNPATHED_ERRORS_SYMBOL = Symbol('subschemaErrors'); +export const INITIAL_PATH_SYMBOL = Symbol('initialPath'); +export const UNPATHED_ERRORS_SYMBOL = Symbol('unpathedErrors'); export const OBJECT_SUBSCHEMA_SYMBOL = Symbol('initialSubschema'); -export const FIELD_SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap'); +export const INITIAL_POSSIBLE_FIELDS = Symbol('initialPossibleFields'); +export const INFO_SYMBOL = Symbol('info'); +export const RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap'); +export const RECEIVER_MAP_SYMBOL = Symbol('receiverMap'); diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 7899ea8ee7c..4a27771dd47 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -11,6 +11,7 @@ import { OperationTypeNode, GraphQLError, GraphQLNamedType, + GraphQLFieldMap, } from 'graphql'; import DataLoader from 'dataloader'; @@ -18,7 +19,14 @@ import DataLoader from 'dataloader'; import { ExecutionRequest, ExecutionResult, Executor } from '@graphql-tools/utils'; import { Subschema } from './Subschema'; -import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; +import { + INFO_SYMBOL, + INITIAL_PATH_SYMBOL, + INITIAL_POSSIBLE_FIELDS, + OBJECT_SUBSCHEMA_SYMBOL, + RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL, + UNPATHED_ERRORS_SYMBOL, +} from './symbols'; export type SchemaTransform> = ( originalWrappingSchema: GraphQLSchema, @@ -52,11 +60,9 @@ export interface DelegationContext> { context?: TContext; info?: GraphQLResolveInfo; returnType: GraphQLOutputType; - onLocatedError?: (originalError: GraphQLError) => GraphQLError; rootValue?: any; transforms: Array>; transformedSchema: GraphQLSchema; - skipTypeMerging: boolean; } export interface IDelegateToSchemaOptions, TArgs = Record> { @@ -75,7 +81,6 @@ export interface IDelegateToSchemaOptions, TArgs transforms?: Array>; transformedSchema?: GraphQLSchema; validateRequest?: boolean; - skipTypeMerging?: boolean; } export interface IDelegateRequestOptions, TArgs = Record> @@ -111,13 +116,15 @@ export interface ICreateRequest { info?: GraphQLResolveInfo; } -export type DelegationPlanBuilder = ( - schema: GraphQLSchema, - sourceSubschema: Subschema, - variableValues: Record, - fragments: Record, - fieldNodes: FieldNode[] -) => Array>; +export interface ExternalValueFromResultOptions> { + result: ExecutionResult; + schema: GraphQLSchema | SubschemaConfig; + fieldName?: string; + context?: TContext; + info?: GraphQLResolveInfo; + returnType?: GraphQLOutputType; + onLocatedError?: (error: GraphQLError) => GraphQLError; +} export interface MergedTypeInfo> { typeName: string; @@ -126,10 +133,10 @@ export interface MergedTypeInfo> { uniqueFields: Record>; nonUniqueFields: Record>>; typeMaps: Map, Record>; + subschemaFields: Record; selectionSets: Map, SelectionSetNode>; fieldSelectionSets: Map, Record>; resolvers: Map, MergedTypeResolver>; - delegationPlanBuilder: DelegationPlanBuilder; } export interface ICreateProxyingResolverOptions> { @@ -153,32 +160,29 @@ export interface SubschemaConfig; rootValue?: any; transforms?: Array>; - merge?: Record>; + merge?: Record>; executor?: Executor; batch?: boolean; batchingOptions?: BatchingOptions; } -export interface MergedTypeConfig> - extends MergedTypeEntryPoint { +export interface MergedTypeConfig> extends MergedTypeEntryPoint { entryPoints?: Array; fields?: Record; computedFields?: Record; canonical?: boolean; } -export interface MergedTypeEntryPoint> - extends MergedTypeResolverOptions { +export interface MergedTypeEntryPoint> extends MergedTypeResolverOptions { selectionSet?: string; key?: (originalResult: any) => K; resolve?: MergedTypeResolver; } -export interface MergedTypeResolverOptions { +export interface MergedTypeResolverOptions { fieldName?: string; args?: (originalResult: any) => Record; argsFromKeys?: (keys: ReadonlyArray) => Record; - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array; } export interface MergedFieldConfig { @@ -207,7 +211,11 @@ export interface StitchingInfo> { export interface ExternalObject> { __typename: string; key: any; + [UNPATHED_ERRORS_SYMBOL]: Array; + [INITIAL_PATH_SYMBOL]: Array; [OBJECT_SUBSCHEMA_SYMBOL]: GraphQLSchema | SubschemaConfig; - [FIELD_SUBSCHEMA_MAP_SYMBOL]: Record>; + [INITIAL_POSSIBLE_FIELDS]: GraphQLFieldMap; + [INFO_SYMBOL]: GraphQLResolveInfo; + [RESPONSE_KEY_SUBSCHEMA_MAP_SYMBOL]: Record>; [UNPATHED_ERRORS_SYMBOL]: Array; } diff --git a/packages/delegate/tests/errors.test.ts b/packages/delegate/tests/errors.test.ts index 1fd6edfa88a..6e2df61d1fd 100644 --- a/packages/delegate/tests/errors.test.ts +++ b/packages/delegate/tests/errors.test.ts @@ -1,13 +1,12 @@ -import { GraphQLError, GraphQLResolveInfo, locatedError, graphql } from 'graphql'; +import { GraphQLError, GraphQLResolveInfo, locatedError, graphql, GraphQLSchema } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ExecutionResult } from '@graphql-tools/utils'; import { stitchSchemas } from '@graphql-tools/stitch'; -import { checkResultAndHandleErrors } from '../src/checkResultAndHandleErrors'; import { UNPATHED_ERRORS_SYMBOL } from '../src/symbols'; -import { getUnpathedErrors } from '../src/mergeFields'; -import { delegateToSchema, defaultMergedResolver, DelegationContext } from '../src'; +import { getUnpathedErrors } from '../src/externalObjects'; +import { delegateToSchema, defaultMergedResolver, externalValueFromResult } from '../src'; class ErrorWithExtensions extends GraphQLError { constructor(message: string, code: string) { @@ -32,32 +31,18 @@ describe('Errors', () => { }); }); - describe('checkResultAndHandleErrors', () => { - const fakeInfo: GraphQLResolveInfo = { - fieldName: "foo", - fieldNodes: [], - returnType: {} as any, - parentType: {} as any, - path: {prev: undefined, key: "foo", typename: undefined } as any, - schema: {} as any, - fragments: {}, - rootValue: {}, - operation: {} as any, - variableValues: {} - } - + describe('externalValueFromResult', () => { test('persists single error', () => { const result = { errors: [new GraphQLError('Test error')], }; try { - checkResultAndHandleErrors( + externalValueFromResult({ result, - { - fieldName: 'responseKey', - info: fakeInfo, - } as DelegationContext, - ); + schema: {} as GraphQLSchema, + fieldName: 'responseKey', + info: { fieldName: 'foo' } as GraphQLResolveInfo, + }); } catch (e: any) { expect(e.message).toEqual('Test error'); expect(e.originalError.errors).toBeUndefined(); @@ -69,13 +54,12 @@ describe('Errors', () => { errors: [new ErrorWithExtensions('Test error', 'UNAUTHENTICATED')], }; try { - checkResultAndHandleErrors( + externalValueFromResult({ result, - { - fieldName: 'responseKey', - info: fakeInfo, - } as DelegationContext, - ); + schema: {} as GraphQLSchema, + fieldName: 'responseKey', + info: { fieldName: 'foo' } as GraphQLResolveInfo, + }); } catch (e: any) { expect(e.message).toEqual('Test error'); expect(e.extensions && e.extensions.code).toEqual('UNAUTHENTICATED'); @@ -88,13 +72,12 @@ describe('Errors', () => { errors: [new GraphQLError('Error1'), new GraphQLError('Error2')], }; try { - checkResultAndHandleErrors( + externalValueFromResult({ result, - { - fieldName: 'responseKey', - info: fakeInfo, - } as DelegationContext, - ); + schema: {} as GraphQLSchema, + fieldName: 'responseKey', + info: { fieldName: 'foo' } as GraphQLResolveInfo, + }); } catch (e: any) { expect(e.message).toEqual('Error1\nError2'); expect(e.originalError).toBeDefined(); diff --git a/packages/stitch/package.json b/packages/stitch/package.json index a1537918e1c..9a0347be9ef 100644 --- a/packages/stitch/package.json +++ b/packages/stitch/package.json @@ -33,7 +33,6 @@ }, "devDependencies": { "dataloader": "2.0.0", - "is-promise": "4.0.0", "value-or-promise": "1.0.10" }, "dependencies": { diff --git a/packages/stitch/src/createDelegationPlanBuilder.ts b/packages/stitch/src/createDelegationPlanBuilder.ts deleted file mode 100644 index 74bfd22b7ed..00000000000 --- a/packages/stitch/src/createDelegationPlanBuilder.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { - FieldNode, - SelectionNode, - Kind, - SelectionSetNode, - GraphQLObjectType, - getNamedType, - GraphQLSchema, - FragmentDefinitionNode, -} from 'graphql'; - -import { DelegationPlanBuilder, MergedTypeInfo, StitchingInfo, Subschema } from '@graphql-tools/delegate'; - -import { getFieldsNotInSubschema } from './getFieldsNotInSubschema'; -import { memoize1, memoize2, memoize3, memoize5 } from '@graphql-tools/utils'; - -function calculateDelegationStage( - mergedTypeInfo: MergedTypeInfo, - sourceSubschemas: Array, - targetSubschemas: Array, - fieldNodes: Array -): { - delegationMap: Map; - proxiableSubschemas: Array; - nonProxiableSubschemas: Array; - unproxiableFieldNodes: Array; -} { - const { selectionSets, fieldSelectionSets, uniqueFields, nonUniqueFields } = mergedTypeInfo; - - // 1. calculate if possible to delegate to given subschema - - const proxiableSubschemas: Array = []; - const nonProxiableSubschemas: Array = []; - - for (const t of targetSubschemas) { - const selectionSet = selectionSets.get(t); - const fieldSelectionSetsMap = fieldSelectionSets.get(t); - if (selectionSet != null && !subschemaTypesContainSelectionSet(mergedTypeInfo, sourceSubschemas, selectionSet)) { - nonProxiableSubschemas.push(t); - } else { - if ( - fieldSelectionSetsMap == null || - fieldNodes.every(fieldNode => { - const fieldName = fieldNode.name.value; - const fieldSelectionSet = fieldSelectionSetsMap[fieldName]; - return ( - fieldSelectionSet == null || - subschemaTypesContainSelectionSet(mergedTypeInfo, sourceSubschemas, fieldSelectionSet) - ); - }) - ) { - proxiableSubschemas.push(t); - } else { - nonProxiableSubschemas.push(t); - } - } - } - - const unproxiableFieldNodes: Array = []; - - // 2. for each selection: - - const delegationMap: Map = new Map(); - for (const fieldNode of fieldNodes) { - if (fieldNode.name.value === '__typename') { - continue; - } - - // 2a. use uniqueFields map to assign fields to subschema if one of possible subschemas - - const uniqueSubschema: Subschema = uniqueFields[fieldNode.name.value]; - if (uniqueSubschema != null) { - if (!proxiableSubschemas.includes(uniqueSubschema)) { - unproxiableFieldNodes.push(fieldNode); - continue; - } - - const existingSubschema = delegationMap.get(uniqueSubschema)?.selections as SelectionNode[]; - if (existingSubschema != null) { - existingSubschema.push(fieldNode); - } else { - delegationMap.set(uniqueSubschema, { - kind: Kind.SELECTION_SET, - selections: [fieldNode], - }); - } - - continue; - } - - // 2b. use nonUniqueFields to assign to a possible subschema, - // preferring one of the subschemas already targets of delegation - - let nonUniqueSubschemas: Array = nonUniqueFields[fieldNode.name.value]; - if (nonUniqueSubschemas == null) { - unproxiableFieldNodes.push(fieldNode); - continue; - } - - nonUniqueSubschemas = nonUniqueSubschemas.filter(s => proxiableSubschemas.includes(s)); - if (!nonUniqueSubschemas.length) { - unproxiableFieldNodes.push(fieldNode); - continue; - } - - const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); - if (existingSubschema != null) { - // It is okay we previously explicitly check whether the map has the element. - (delegationMap.get(existingSubschema)!.selections as SelectionNode[]).push(fieldNode); - } else { - delegationMap.set(nonUniqueSubschemas[0], { - kind: Kind.SELECTION_SET, - selections: [fieldNode], - }); - } - } - - return { - delegationMap, - proxiableSubschemas, - nonProxiableSubschemas, - unproxiableFieldNodes, - }; -} - -function getStitchingInfo(schema: GraphQLSchema): StitchingInfo { - const stitchingInfo = schema.extensions?.['stitchingInfo'] as StitchingInfo | undefined; - if (!stitchingInfo) { - throw new Error(`Schema is not a stitched schema.`); - } - return stitchingInfo; -} - -export function createDelegationPlanBuilder(mergedTypeInfo: MergedTypeInfo): DelegationPlanBuilder { - return memoize5(function delegationPlanBuilder( - schema: GraphQLSchema, - sourceSubschema: Subschema, - variableValues: Record, - fragments: Record, - fieldNodes: FieldNode[] - ): Array> { - const stitchingInfo = getStitchingInfo(schema); - const targetSubschemas = mergedTypeInfo?.targetSubschemas.get(sourceSubschema); - if (!targetSubschemas || !targetSubschemas.length) { - return []; - } - - const typeName = mergedTypeInfo.typeName; - const fieldsNotInSubschema = getFieldsNotInSubschema( - schema, - stitchingInfo, - schema.getType(typeName) as GraphQLObjectType, - mergedTypeInfo.typeMaps.get(sourceSubschema)?.[typeName] as GraphQLObjectType, - fieldNodes, - fragments, - variableValues - ); - - if (!fieldsNotInSubschema.length) { - return []; - } - - const delegationMaps: Array> = []; - let sourceSubschemas = createSubschemas(sourceSubschema); - - let delegationStage = calculateDelegationStage( - mergedTypeInfo, - sourceSubschemas, - targetSubschemas, - fieldsNotInSubschema - ); - let { delegationMap } = delegationStage; - while (delegationMap.size) { - delegationMaps.push(delegationMap); - - const { proxiableSubschemas, nonProxiableSubschemas, unproxiableFieldNodes } = delegationStage; - - sourceSubschemas = combineSubschemas(sourceSubschemas, proxiableSubschemas); - - delegationStage = calculateDelegationStage( - mergedTypeInfo, - sourceSubschemas, - nonProxiableSubschemas, - unproxiableFieldNodes - ); - delegationMap = delegationStage.delegationMap; - } - - return delegationMaps; - }); -} - -const createSubschemas = memoize1(function createSubschemas(sourceSubschema: Subschema): Array { - return [sourceSubschema]; -}); - -const combineSubschemas = memoize2(function combineSubschemas( - sourceSubschemas: Array, - additionalSubschemas: Array -): Array { - return sourceSubschemas.concat(additionalSubschemas); -}); - -const subschemaTypesContainSelectionSet = memoize3(function subschemaTypesContainSelectionSet( - mergedTypeInfo: MergedTypeInfo, - sourceSubchemas: Array, - selectionSet: SelectionSetNode -) { - return typesContainSelectionSet( - sourceSubchemas.map( - sourceSubschema => sourceSubschema.transformedSchema.getType(mergedTypeInfo.typeName) as GraphQLObjectType - ), - selectionSet - ); -}); - -function typesContainSelectionSet(types: Array, selectionSet: SelectionSetNode): boolean { - const fieldMaps = types.map(type => type.getFields()); - - for (const selection of selectionSet.selections) { - if (selection.kind === Kind.FIELD) { - const fields = fieldMaps.map(fieldMap => fieldMap[selection.name.value]).filter(field => field != null); - if (!fields.length) { - return false; - } - - if (selection.selectionSet != null) { - return typesContainSelectionSet( - fields.map(field => getNamedType(field.type)) as Array, - selection.selectionSet - ); - } - } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.typeCondition?.name.value === types[0].name) { - return typesContainSelectionSet(types, selection.selectionSet); - } - } - - return true; -} diff --git a/packages/stitch/src/createMergedTypeResolver.ts b/packages/stitch/src/createMergedTypeResolver.ts index 76735bf923b..c2a350905db 100644 --- a/packages/stitch/src/createMergedTypeResolver.ts +++ b/packages/stitch/src/createMergedTypeResolver.ts @@ -1,11 +1,11 @@ -import { getNamedType, GraphQLOutputType, GraphQLList } from 'graphql'; +import { getNamedType, GraphQLOutputType } from 'graphql'; import { delegateToSchema, MergedTypeResolver, MergedTypeResolverOptions } from '@graphql-tools/delegate'; import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; export function createMergedTypeResolver( mergedTypeResolverOptions: MergedTypeResolverOptions ): MergedTypeResolver | undefined { - const { fieldName, argsFromKeys, valuesFromResults, args } = mergedTypeResolverOptions; + const { fieldName, argsFromKeys, args } = mergedTypeResolverOptions; if (argsFromKeys != null) { return function mergedBatchedTypeResolver(originalResult, context, info, subschema, selectionSet, key) { @@ -13,16 +13,14 @@ export function createMergedTypeResolver( schema: subschema, operation: 'query', fieldName, - returnType: new GraphQLList( - getNamedType(info.schema.getType(originalResult.__typename) ?? info.returnType) as GraphQLOutputType - ), + returnType: getNamedType( + info.schema.getType(originalResult.__typename) ?? info.returnType + ) as GraphQLOutputType, key, argsFromKeys, - valuesFromResults, selectionSet, context, info, - skipTypeMerging: true, }); }; } @@ -40,7 +38,6 @@ export function createMergedTypeResolver( selectionSet, context, info, - skipTypeMerging: true, }); }; } diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index ed02dd37c5d..86acbec4bce 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -5,13 +5,21 @@ import { GraphQLDirective, specifiedDirectives, extendSchema, + isObjectType, } from 'graphql'; -import { IResolvers, pruneSchema } from '@graphql-tools/utils'; +import { IObjectTypeResolver, IResolvers, pruneSchema } from '@graphql-tools/utils'; import { addResolversToSchema, assertResolversPresent, extendResolversFromInterfaces } from '@graphql-tools/schema'; -import { SubschemaConfig, isSubschemaConfig, Subschema, defaultMergedResolver } from '@graphql-tools/delegate'; +import { + SubschemaConfig, + isSubschemaConfig, + Subschema, + defaultMergedResolver, + isExternalObject, + getMergedParent, +} from '@graphql-tools/delegate'; import { IStitchSchemasOptions, SubschemaConfigTransform } from './types'; @@ -131,11 +139,13 @@ export function stitchSchemas>({ // We allow passing in an array of resolver maps, in which case we merge them const resolverMap: IResolvers = mergeResolvers(resolvers); - const finalResolvers = inheritResolversFromInterfaces + const extendedResolvers = inheritResolversFromInterfaces ? extendResolversFromInterfaces(schema, resolverMap) : resolverMap; - stitchingInfo = completeStitchingInfo(stitchingInfo, finalResolvers, schema); + stitchingInfo = completeStitchingInfo(stitchingInfo, extendedResolvers, schema); + + const finalResolvers = wrapResolvers(extendedResolvers, schema); schema = addResolversToSchema({ schema, @@ -214,3 +224,42 @@ function applySubschemaConfigTransforms>( return transformedSubschemas; } + +function wrapResolvers(originalResolvers: IResolvers, schema: GraphQLSchema): IResolvers { + const wrappedResolvers: IResolvers = Object.create(null); + + Object.keys(originalResolvers).forEach(typeName => { + const typeEntry = originalResolvers[typeName]; + const type = schema.getType(typeName); + if (!isObjectType(type)) { + wrappedResolvers[typeName] = originalResolvers[typeName]; + return; + } + + const newTypeEntry: IObjectTypeResolver = Object.create(null); + Object.keys(typeEntry).forEach(fieldName => { + const field = typeEntry[fieldName]; + const originalResolver = field?.resolve; + if (originalResolver === undefined) { + newTypeEntry[fieldName] = field; + return; + } + + newTypeEntry[fieldName] = { + ...field, + resolve: (parent, args, context, info) => { + if (!isExternalObject(parent)) { + return originalResolver(parent, args, context, info); + } + + return getMergedParent(parent, context, info).then(mergedParent => + originalResolver(mergedParent, args, context, info) + ); + }, + }; + }); + wrappedResolvers[typeName] = newTypeEntry; + }); + + return wrappedResolvers; +} diff --git a/packages/stitch/src/stitchingInfo.ts b/packages/stitch/src/stitchingInfo.ts index 05f9b5fd1cb..4fd618b62a9 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -22,7 +22,6 @@ import { MergedTypeResolver, Subschema, SubschemaConfig, MergedTypeInfo, Stitchi import { MergeTypeCandidate, MergeTypeFilter } from './types'; import { createMergedTypeResolver } from './createMergedTypeResolver'; -import { createDelegationPlanBuilder } from './createDelegationPlanBuilder'; export function createStitchingInfo>( subschemaMap: Map, Subschema>, @@ -165,19 +164,17 @@ function createMergedTypes>( fieldSelectionSets, uniqueFields: Object.create({}), nonUniqueFields: Object.create({}), + subschemaFields: Object.create({}), resolvers, } as MergedTypeInfo; - mergedTypes[typeName].delegationPlanBuilder = createDelegationPlanBuilder( - mergedTypes[typeName] as MergedTypeInfo - ); - for (const fieldName in supportedBySubschemas) { if (supportedBySubschemas[fieldName].length === 1) { mergedTypes[typeName].uniqueFields[fieldName] = supportedBySubschemas[fieldName][0]; } else { mergedTypes[typeName].nonUniqueFields[fieldName] = supportedBySubschemas[fieldName]; } + mergedTypes[typeName].subschemaFields[fieldName] = true; } } } diff --git a/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts b/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts index 493772bbfc9..3c7465cb29a 100644 --- a/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts +++ b/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts @@ -13,7 +13,7 @@ export function splitMergedTypeEntryPointsTransformer(subschemaConfig: Subschema for (let i = 0; i < maxEntryPoints; i += 1) { const subschemaPermutation = cloneSubschemaConfig(subschemaConfig); - const mergedTypesCopy: Record> = subschemaPermutation.merge ?? + const mergedTypesCopy: Record> = subschemaPermutation.merge ?? Object.create(null); let currentMerge = mergedTypesCopy; diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index f7ebed4bcc7..b2ddabbd7e4 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -107,7 +107,6 @@ export type OnTypeConflict> = ( declare module '@graphql-tools/utils' { interface IFieldResolverOptions { - fragment?: string; selectionSet?: string | ((node: FieldNode) => SelectionSetNode); } } diff --git a/packages/stitch/tests/alternateStitchSchemas.test.ts b/packages/stitch/tests/alternateStitchSchemas.test.ts index 7cbd2e4ee5f..c8731131a23 100644 --- a/packages/stitch/tests/alternateStitchSchemas.test.ts +++ b/packages/stitch/tests/alternateStitchSchemas.test.ts @@ -872,7 +872,7 @@ type Query { }); describe('rename nested object fields with interfaces', () => { - test('should work', () => { + test('should work', async () => { const originalNode = { aList: [ { @@ -973,8 +973,8 @@ describe('rename nested object fields with interfaces', () => { } `; - const originalResult = graphqlSync({ schema: originalSchema, source: originalQuery }); - const transformedResult = graphqlSync({ schema: transformedSchema, source: transformedQuery }); + const originalResult = await graphql({ schema: originalSchema, source: originalQuery }); + const transformedResult = await graphql({ schema: transformedSchema, source: transformedQuery }); expect(originalResult).toEqual({ data: { node: originalNode } }); expect(transformedResult).toEqual({ @@ -2014,7 +2014,6 @@ describe('basic type merging', () => { selectionSet, context, info, - skipTypeMerging: true, }), }, }, @@ -2034,7 +2033,6 @@ describe('basic type merging', () => { selectionSet, context, info, - skipTypeMerging: true, }), }, }, @@ -2158,7 +2156,6 @@ describe('unidirectional type merging', () => { selectionSet, context, info, - skipTypeMerging: true, }), }, }, diff --git a/packages/stitch/tests/unknownType.test.ts b/packages/stitch/tests/unknownType.test.ts index 12f774f9a99..74bd097da54 100644 --- a/packages/stitch/tests/unknownType.test.ts +++ b/packages/stitch/tests/unknownType.test.ts @@ -2,6 +2,7 @@ import { graphql, GraphQLError, GraphQLSchema, + versionInfo, } from 'graphql'; import { delegateToSchema } from '@graphql-tools/delegate'; @@ -99,11 +100,16 @@ describe('test delegateToSchema() with type renaming', () => { }, }); + const errorMessagesByVersion = { + 14: `Abstract type ClassicItemInterface must resolve to an Object type at runtime for field Query.itemByVariant with value { __typename: "Item", id: "123", name: "Foo bar 42" }, received "undefined". Either the ClassicItemInterface type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + 15: `Abstract type "ClassicItemInterface" was resolve to a type "Item" that does not exist inside schema.`, + } + expect(result).toEqual({ data: { itemByVariant: null, }, - errors: [new GraphQLError(`Unable to resolve type 'Item'. Did you forget to include a transform that renames types? Did you delegate to the original subschema rather that the subschema config object containing the transform?`)], + errors: [new GraphQLError(errorMessagesByVersion[versionInfo.major])], }); }); diff --git a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts index 570aed10ac2..0d3c86ed015 100644 --- a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts +++ b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts @@ -349,8 +349,7 @@ export function stitchingDirectivesTransformer( for (const typeName in selectionSetsByType) { const selectionSet = selectionSetsByType[typeName]; - const mergeConfig: Record> = newSubschemaConfig.merge ?? - Object.create(null); + const mergeConfig: Record> = newSubschemaConfig.merge ?? Object.create(null); newSubschemaConfig.merge = mergeConfig; if (mergeConfig[typeName] == null) { @@ -364,8 +363,7 @@ export function stitchingDirectivesTransformer( for (const typeName in computedFieldSelectionSets) { const selectionSets = computedFieldSelectionSets[typeName]; - const mergeConfig: Record> = newSubschemaConfig.merge ?? - Object.create(null); + const mergeConfig: Record> = newSubschemaConfig.merge ?? Object.create(null); newSubschemaConfig.merge = mergeConfig; if (mergeConfig[typeName] == null) { @@ -389,8 +387,7 @@ export function stitchingDirectivesTransformer( for (const typeName in mergedTypesResolversInfo) { const mergedTypeResolverInfo = mergedTypesResolversInfo[typeName]; - const mergeConfig: Record> = newSubschemaConfig.merge ?? - Object.create(null); + const mergeConfig: Record> = newSubschemaConfig.merge ?? Object.create(null); newSubschemaConfig.merge = mergeConfig; if (newSubschemaConfig.merge[typeName] == null) { @@ -411,8 +408,7 @@ export function stitchingDirectivesTransformer( for (const typeName in canonicalTypesInfo) { const canonicalTypeInfo = canonicalTypesInfo[typeName]; - const mergeConfig: Record> = newSubschemaConfig.merge ?? - Object.create(null); + const mergeConfig: Record> = newSubschemaConfig.merge ?? Object.create(null); newSubschemaConfig.merge = mergeConfig; if (newSubschemaConfig.merge[typeName] == null) { diff --git a/packages/utils/src/memoize.ts b/packages/utils/src/memoize.ts index 7f68a4133d0..7e5603e097d 100644 --- a/packages/utils/src/memoize.ts +++ b/packages/utils/src/memoize.ts @@ -1,10 +1,10 @@ export function memoize1 any>(fn: F): F { - const memoize1cache: WeakMap, WeakMap, any>> = new WeakMap(); + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); return function memoized(a1: any): any { - const cachedValue = memoize1cache.get(a1); + const cachedValue = cache1.get(a1); if (cachedValue === undefined) { const newValue = fn(a1); - memoize1cache.set(a1, newValue); + cache1.set(a1, newValue); return newValue; } @@ -13,12 +13,12 @@ export function memoize1 any>(fn: F): F { } export function memoize2 any>(fn: F): F { - const memoize2cache: WeakMap, WeakMap, any>> = new WeakMap(); + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); return function memoized(a1: any, a2: any): any { - let cache2 = memoize2cache.get(a1); + let cache2 = cache1.get(a1); if (!cache2) { cache2 = new WeakMap(); - memoize2cache.set(a1, cache2); + cache1.set(a1, cache2); const newValue = fn(a1, a2); cache2.set(a2, newValue); return newValue; @@ -36,12 +36,12 @@ export function memoize2 any>(fn: F): F { } export function memoize3 any>(fn: F): F { - const memoize3Cache: WeakMap, WeakMap, any>> = new WeakMap(); + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); return function memoized(a1: any, a2: any, a3: any) { - let cache2 = memoize3Cache.get(a1); + let cache2 = cache1.get(a1); if (!cache2) { cache2 = new WeakMap(); - memoize3Cache.set(a1, cache2); + cache1.set(a1, cache2); const cache3 = new WeakMap(); cache2.set(a2, cache3); const newValue = fn(a1, a2, a3); @@ -70,12 +70,12 @@ export function memoize3 any>(fn: F): F } export function memoize4 any>(fn: F): F { - const memoize4Cache: WeakMap, WeakMap, any>> = new WeakMap(); + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); return function memoized(a1: any, a2: any, a3: any, a4: any) { - let cache2 = memoize4Cache.get(a1); + let cache2 = cache1.get(a1); if (!cache2) { cache2 = new WeakMap(); - memoize4Cache.set(a1, cache2); + cache1.set(a1, cache2); const cache3 = new WeakMap(); cache2.set(a2, cache3); const cache4 = new WeakMap(); @@ -117,12 +117,12 @@ export function memoize4 any>( } export function memoize5 any>(fn: F): F { - const memoize5Cache: WeakMap, WeakMap, any>> = new WeakMap(); + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); return function memoized(a1: any, a2: any, a3: any, a4: any, a5: any) { - let cache2 = memoize5Cache.get(a1); + let cache2 = cache1.get(a1); if (!cache2) { cache2 = new WeakMap(); - memoize5Cache.set(a1, cache2); + cache1.set(a1, cache2); const cache3 = new WeakMap(); cache2.set(a2, cache3); const cache4 = new WeakMap(); @@ -178,13 +178,27 @@ export function memoize5, WeakMap, any>> = new WeakMap(); +export function memoize1of2 any>(fn: F): F { + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); + return function memoized(a1: any, a2: any): any { + const cachedValue = cache1.get(a1); + if (cachedValue === undefined) { + const newValue = fn(a1, a2); + cache1.set(a1, newValue); + return newValue; + } + + return cachedValue; + } as F; +} + export function memoize2of4 any>(fn: F): F { + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); return function memoized(a1: any, a2: any, a3: any, a4: any): any { - let cache2 = memoize2of4cache.get(a1); + let cache2 = cache1.get(a1); if (!cache2) { cache2 = new WeakMap(); - memoize2of4cache.set(a1, cache2); + cache1.set(a1, cache2); const newValue = fn(a1, a2, a3, a4); cache2.set(a2, newValue); return newValue; @@ -200,3 +214,50 @@ export function memoize2of4 an return cachedValue; } as F; } + +export function memoize4ofMany) => any>(fn: F): F { + const cache1: WeakMap, WeakMap, any>> = new WeakMap(); + return function memoized(a1: any, a2: any, a3: any, a4: any, ...args: Array) { + let cache2 = cache1.get(a1); + if (!cache2) { + cache2 = new WeakMap(); + cache1.set(a1, cache2); + const cache3 = new WeakMap(); + cache2.set(a2, cache3); + const cache4 = new WeakMap(); + cache3.set(a3, cache4); + const newValue = fn(a1, a2, a3, a4, ...args); + cache4.set(a4, newValue); + return newValue; + } + + let cache3 = cache2.get(a2); + if (!cache3) { + cache3 = new WeakMap(); + cache2.set(a2, cache3); + const cache4 = new WeakMap(); + cache3.set(a3, cache4); + const newValue = fn(a1, a2, a3, a4, ...args); + cache4.set(a4, newValue); + return newValue; + } + + const cache4 = cache3.get(a3); + if (!cache4) { + const cache4 = new WeakMap(); + cache3.set(a3, cache4); + const newValue = fn(a1, a2, a3, a4, ...args); + cache4.set(a4, newValue); + return newValue; + } + + const cachedValue = cache4.get(a4); + if (cachedValue === undefined) { + const newValue = fn(a1, a2, a3, a4, ...args); + cache4.set(a4, newValue); + return newValue; + } + + return cachedValue; + } as F; +} diff --git a/packages/wrap/package.json b/packages/wrap/package.json index 4095fa60ae0..00a7c963185 100644 --- a/packages/wrap/package.json +++ b/packages/wrap/package.json @@ -31,9 +31,6 @@ "buildOptions": { "input": "./src/index.ts" }, - "devDependencies": { - "is-promise": "4.0.0" - }, "dependencies": { "@graphql-tools/delegate": "^8.2.0", "@graphql-tools/schema": "^8.2.0", diff --git a/packages/wrap/src/generateProxyingResolvers.ts b/packages/wrap/src/generateProxyingResolvers.ts index cc121d8662b..cba31c92baa 100644 --- a/packages/wrap/src/generateProxyingResolvers.ts +++ b/packages/wrap/src/generateProxyingResolvers.ts @@ -4,12 +4,13 @@ import { getResponseKeyFromInfo, getRootTypeMap } from '@graphql-tools/utils'; import { delegateToSchema, getSubschema, - resolveExternalValue, SubschemaConfig, ICreateProxyingResolverOptions, applySchemaTransforms, isExternalObject, getUnpathedErrors, + getInitialPath, + createExternalValue, } from '@graphql-tools/delegate'; export function generateProxyingResolvers( @@ -68,14 +69,15 @@ function createPossiblyNestedProxyingResolver( // Check to see if the parent contains a proxied result if (isExternalObject(parent)) { - const unpathedErrors = getUnpathedErrors(parent); const subschema = getSubschema(parent, responseKey); // If there is a proxied result from this subschema, return it // This can happen even for a root field when the root type ia // also nested as a field within a different type. if (subschemaConfig === subschema && parent[responseKey] !== undefined) { - return resolveExternalValue(parent[responseKey], unpathedErrors, subschema, context, info); + const unpathedErrors = getUnpathedErrors(parent); + const initialPath = getInitialPath(parent); + return createExternalValue(parent[responseKey], unpathedErrors, initialPath, subschema, context, info); } } }