diff --git a/packages/stitch/src/createMergedTypeResolver.ts b/packages/stitch/src/createMergedTypeResolver.ts index 76735bf923b..a096f1f1e81 100644 --- a/packages/stitch/src/createMergedTypeResolver.ts +++ b/packages/stitch/src/createMergedTypeResolver.ts @@ -1,4 +1,6 @@ import { getNamedType, GraphQLOutputType, GraphQLList } from 'graphql'; +import { ValueOrPromise } from 'value-or-promise'; + import { delegateToSchema, MergedTypeResolver, MergedTypeResolverOptions } from '@graphql-tools/delegate'; import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; @@ -29,19 +31,23 @@ export function createMergedTypeResolver( if (args != null) { return function mergedTypeResolver(originalResult, context, info, subschema, selectionSet) { - return delegateToSchema({ - schema: subschema, - operation: 'query', - fieldName, - returnType: getNamedType( - info.schema.getType(originalResult.__typename) ?? info.returnType - ) as GraphQLOutputType, - args: args(originalResult), - selectionSet, - context, - info, - skipTypeMerging: true, - }); + return new ValueOrPromise(() => args(originalResult)) + .then(resolvedArgs => + delegateToSchema({ + schema: subschema, + operation: 'query', + fieldName, + returnType: getNamedType( + info.schema.getType(originalResult.__typename) ?? info.returnType + ) as GraphQLOutputType, + args: resolvedArgs, + selectionSet, + context, + info, + skipTypeMerging: true, + }) + ) + .resolve(); }; } diff --git a/packages/stitch/src/stitchingInfo.ts b/packages/stitch/src/stitchingInfo.ts index 05f9b5fd1cb..10bdc392e32 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -15,6 +15,8 @@ import { GraphQLNamedType, } from 'graphql'; +import { ValueOrPromise } from 'value-or-promise'; + import { collectFields, parseSelectionSet, IResolvers, IFieldResolverOptions, isSome } from '@graphql-tools/utils'; import { MergedTypeResolver, Subschema, SubschemaConfig, MergedTypeInfo, StitchingInfo } from '@graphql-tools/delegate'; @@ -119,8 +121,9 @@ function createMergedTypes>( subschema, keyFn ? (originalResult, context, info, subschema, selectionSet) => { - const key = keyFn(originalResult); - return resolver(originalResult, context, info, subschema, selectionSet, key); + return new ValueOrPromise(() => keyFn(originalResult)) + .then(key => resolver(originalResult, context, info, subschema, selectionSet, key)) + .resolve(); } : resolver ); diff --git a/packages/stitching-directives/src/properties.ts b/packages/stitching-directives/src/properties.ts index f33aaec9066..ef8a513c266 100644 --- a/packages/stitching-directives/src/properties.ts +++ b/packages/stitching-directives/src/properties.ts @@ -1,3 +1,5 @@ +import { ValueOrPromise } from 'value-or-promise'; + import { PropertyTree } from './types'; export function addProperty(object: Record, path: Array, value: any) { @@ -62,6 +64,35 @@ export function getProperties(object: Record, propertyTree: Propert return newObject; } +// c.f. https://github.com/graphql/graphql-js/blob/main/src/jsutils/promiseForObject.ts +export function getResolvedPropertiesOrPromise( + object: Record, + propertyTree: PropertyTree +): ValueOrPromise { + if (object == null) { + return new ValueOrPromise(() => object); + } + + const keys = Object.keys(propertyTree); + + const newObject = Object.create(null); + return ValueOrPromise.all( + keys.map(key => + new ValueOrPromise(() => object[key]).then(value => { + const subKey = propertyTree[key]; + if (subKey == null) { + newObject[key] = value; + return; + } + + newObject[key] = deepMap(value, function deepMapFn(item) { + return getProperties(item, subKey); + }); + }) + ) + ).then(() => newObject); +} + export function propertyTreeFromPaths(paths: Array>): PropertyTree { const propertyTree = Object.create(null); for (const path of paths) { diff --git a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts index 570aed10ac2..6035a932cf5 100644 --- a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts +++ b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts @@ -29,7 +29,7 @@ import { MergedTypeResolverInfo, StitchingDirectivesOptions } from './types'; import { defaultStitchingDirectiveOptions } from './defaultStitchingDirectiveOptions'; import { parseMergeArgsExpr } from './parseMergeArgsExpr'; -import { addProperty, getProperty, getProperties } from './properties'; +import { addProperty, getProperty, getResolvedPropertiesOrPromise } from './properties'; import { stitchingDirectivesValidator } from './stitchingDirectivesValidator'; export function stitchingDirectivesTransformer( @@ -465,8 +465,8 @@ function forEachConcreteType( } function generateKeyFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (originalResult: any) => any { - return function keyFn(originalResult: any) { - return getProperties(originalResult, mergedTypeResolverInfo.usedProperties); + return function keyFn(originalResult: any): any { + return getResolvedPropertiesOrPromise(originalResult, mergedTypeResolverInfo.usedProperties).resolve(); }; } @@ -498,19 +498,24 @@ function generateArgsFromKeysFn( }; } -function generateArgsFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (originalResult: any) => Record { +function generateArgsFn( + mergedTypeResolverInfo: MergedTypeResolverInfo +): (originalResult: any) => Record | Promise> { const { mappingInstructions, args, usedProperties } = mergedTypeResolverInfo; - return function generateArgs(originalResult: any): Record { - const newArgs = mergeDeep([{}, args]); - const filteredResult = getProperties(originalResult, usedProperties); - if (mappingInstructions) { - for (const mappingInstruction of mappingInstructions) { - const { destinationPath, sourcePath } = mappingInstruction; - addProperty(newArgs, destinationPath, getProperty(filteredResult, sourcePath)); - } - } - return newArgs; + return function generateArgs(originalResult: any): Record | Promise> { + return getResolvedPropertiesOrPromise(originalResult, usedProperties) + .then(filteredResult => { + const newArgs = mergeDeep([{}, args]); + if (mappingInstructions) { + for (const mappingInstruction of mappingInstructions) { + const { destinationPath, sourcePath } = mappingInstruction; + addProperty(newArgs, destinationPath, getProperty(filteredResult, sourcePath)); + } + } + return newArgs; + }) + .resolve(); }; } diff --git a/packages/stitching-directives/tests/properties.test.ts b/packages/stitching-directives/tests/properties.test.ts index 5a34543e5e6..db1b1e838f3 100644 --- a/packages/stitching-directives/tests/properties.test.ts +++ b/packages/stitching-directives/tests/properties.test.ts @@ -1,4 +1,4 @@ -import { addProperty, getProperties } from "../src/properties"; +import { addProperty, getProperties, getResolvedPropertiesOrPromise } from "../src/properties"; describe('addProperty', () => { test('can add a key to an object', () => { @@ -29,7 +29,7 @@ describe('addProperty', () => { }); describe('getProperties', () => { - test('can getProperties', () => { + test('can getProperties', async () => { const object = { field1: 'value1', field2: { @@ -43,7 +43,35 @@ describe('getProperties', () => { field2: { subfieldA: null, } - }) + }); + + const expectedExtracted = { + field1: 'value1', + field2: { + subfieldA: 'valueA', + } + } + + expect(extracted).toEqual(expectedExtracted); + }); +}); + +describe('getResolvedPropertiesOrPromise', () => { + test('can getResolvedPropertiesOrPromise', async () => { + const object = { + field1: 'value1', + field2: Promise.resolve({ + subfieldA: 'valueA', + subfieldB: 'valueB', + }), + } + + const extracted = await getResolvedPropertiesOrPromise(object, { + field1: null, + field2: { + subfieldA: null, + } + }).resolve(); const expectedExtracted = { field1: 'value1',