From 5b7320edf21af41db253d1a3730da062175f9179 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 21:56:20 +0300 Subject: [PATCH 01/49] update graphql-js version to support defer/stream --- .github/workflows/tests.yml | 28 ++-- package.json | 4 +- ...l+15.4.0-experimental-stream-defer.1.patch | 143 ++++++++++++++++++ scripts/match-graphql.js | 8 +- 4 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 patches/graphql+15.4.0-experimental-stream-defer.1.patch diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcd0e515c36..afd4cbddc56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,11 +31,11 @@ jobs: - name: Lint run: yarn lint build: - name: Build on ${{matrix.os}} GraphQL v${{matrix.graphql_version}} + name: Build on ${{matrix.os}} GraphQL ${{matrix.graphql_version_or_tag}} runs-on: ubuntu-latest strategy: matrix: - graphql_version: [14, 15] + graphql_version_or_tag: [experimental-stream-defer] steps: - name: Checkout Master uses: actions/checkout@v2 @@ -47,23 +47,23 @@ jobs: uses: actions/cache@v2 with: path: '**/node_modules' - key: ${{ runner.os }}-16-${{matrix.graphql_version}}-yarn-${{ hashFiles('yarn.lock') }} + key: ${{ runner.os }}-16-${{matrix.graphql_version_or_tag}}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | - ${{ runner.os }}-16-${{matrix.graphql_version}}-yarn - - name: Use GraphQL v${{matrix.graphql_version}} - run: node ./scripts/match-graphql.js ${{matrix.graphql_version}} + ${{ runner.os }}-16-${{matrix.graphql_version_or_tag}}-yarn + - name: Use GraphQL ${{matrix.graphql_version_or_tag}} + run: node ./scripts/match-graphql.js ${{matrix.graphql_version_or_tag}} - name: Install Dependencies using Yarn run: yarn install --ignore-engines && git checkout yarn.lock - name: Build run: yarn ts:transpile test: - name: Test on ${{matrix.os}}, Node ${{matrix.node_version}} and GraphQL v${{matrix.graphql_version}} + name: Test on ${{matrix.os}}, Node ${{matrix.node_version}} and GraphQL ${{matrix.graphql_version_or_tag}} runs-on: ${{matrix.os}} strategy: matrix: os: [ubuntu-latest] # remove windows to speed up the tests node_version: [10, 16] - graphql_version: [14, 15] + graphql_version_or_tag: [experimental-stream-defer] steps: - name: Checkout Master uses: actions/checkout@v2 @@ -75,20 +75,20 @@ jobs: uses: actions/cache@v2 with: path: '**/node_modules' - key: ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version}}-yarn-${{ hashFiles('yarn.lock') }} + key: ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version_or_tag}}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | - ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version}}-yarn - - name: Use GraphQL v${{matrix.graphql_version}} - run: node ./scripts/match-graphql.js ${{matrix.graphql_version}} + ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version_or_tag}}-yarn + - name: Use GraphQL ${{matrix.graphql_version_or_tag}} + run: node ./scripts/match-graphql.js ${{matrix.graphql_version_or_tag}} - name: Install Dependencies using Yarn run: yarn install --ignore-engines && git checkout yarn.lock - name: Cache Jest uses: actions/cache@v2 with: path: .cache/jest - key: ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version}}-jest-${{ hashFiles('yarn.lock') }} + key: ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version_or_tag}}-jest-${{ hashFiles('yarn.lock') }} restore-keys: | - ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version}}-jest- + ${{ runner.os }}-${{matrix.node_version}}-${{matrix.graphql_version_or_tag}}-jest- - name: Test run: yarn test --ci env: diff --git a/package.json b/package.json index c14381bb125..b4e76acb6bc 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "5.1.0", "eslint-plugin-standard": "5.0.0", - "graphql": "15.5.0", + "graphql": "15.4.0-experimental-stream-defer.1", "graphql-helix": "1.6.1", "graphql-subscriptions": "1.2.1", "husky": "6.0.0", @@ -93,7 +93,7 @@ ] }, "resolutions": { - "graphql": "15.5.0", + "graphql": "15.4.0-experimental-stream-defer.1", "@changesets/apply-release-plan": "5.0.0" } } diff --git a/patches/graphql+15.4.0-experimental-stream-defer.1.patch b/patches/graphql+15.4.0-experimental-stream-defer.1.patch new file mode 100644 index 00000000000..7508e678639 --- /dev/null +++ b/patches/graphql+15.4.0-experimental-stream-defer.1.patch @@ -0,0 +1,143 @@ +diff --git a/node_modules/graphql/execution/execute.js b/node_modules/graphql/execution/execute.js +index e711fb5..e1dc791 100644 +--- a/node_modules/graphql/execution/execute.js ++++ b/node_modules/graphql/execution/execute.js +@@ -724,7 +724,13 @@ function completeAsyncIteratorValue(exeContext, itemType, fieldNodes, info, path + var containsPromise = false; + var stream = getStreamValues(exeContext, fieldNodes); + return new Promise(function (resolve) { +- function next(index, completedResults) { ++ function advance(index, completedResults) { ++ if (stream && typeof stream.initialCount === 'number' && index >= stream.initialCount) { ++ exeContext.dispatcher.addAsyncIteratorValue(stream.label, index, path, iterator, exeContext, fieldNodes, info, itemType); ++ resolve(completedResults); ++ return; ++ } ++ + var fieldPath = (0, _Path.addPath)(path, index, undefined); + iterator.next().then(function (_ref) { + var value = _ref.value, +@@ -748,19 +754,9 @@ function completeAsyncIteratorValue(exeContext, itemType, fieldNodes, info, path + completedResults.push(null); + var error = (0, _locatedError.locatedError)(rawError, fieldNodes, (0, _Path.pathToArray)(fieldPath)); + handleFieldError(error, itemType, errors); +- resolve(completedResults); +- return; + } + +- var newIndex = index + 1; +- +- if (stream && typeof stream.initialCount === 'number' && newIndex >= stream.initialCount) { +- exeContext.dispatcher.addAsyncIteratorValue(stream.label, newIndex, path, iterator, exeContext, fieldNodes, info, itemType); +- resolve(completedResults); +- return; +- } +- +- next(newIndex, completedResults); ++ advance(index + 1, completedResults); + }, function (rawError) { + completedResults.push(null); + var error = (0, _locatedError.locatedError)(rawError, fieldNodes, (0, _Path.pathToArray)(fieldPath)); +@@ -769,7 +765,7 @@ function completeAsyncIteratorValue(exeContext, itemType, fieldNodes, info, path + }); + } + +- next(0, []); ++ advance(0, []); + }).then(function (completedResults) { + return containsPromise ? Promise.all(completedResults) : completedResults; + }); +@@ -1187,46 +1183,59 @@ var Dispatcher = /*#__PURE__*/function () { + var _this = this; + + return new Promise(function (resolve) { ++ var resolved = false; + _this._subsequentPayloads.forEach(function (promise) { +- promise.then(function () { +- // resolve with actual promise, not resolved value of promise so we can remove it from this._subsequentPayloads +- resolve({ +- promise: promise +- }); +- }); +- }); +- }).then(function (_ref3) { +- var promise = _ref3.promise; ++ promise.then(function (payload) { ++ if (resolved) { ++ return; ++ } ++ resolved = true; + +- _this._subsequentPayloads.splice(_this._subsequentPayloads.indexOf(promise), 1); ++ if (_this._subsequentPayloads.length === 0) { ++ // a different call to next has exhausted all payloads ++ resolve({ value: undefined, done: true }); ++ return; ++ } + +- return promise; +- }).then(function (_ref4) { +- var value = _ref4.value, +- done = _ref4.done; ++ var index = _this._subsequentPayloads.indexOf(promise); + +- if (done && _this._subsequentPayloads.length === 0) { +- // async iterable resolver just finished and no more pending payloads +- return { +- value: { +- hasNext: false +- }, +- done: false +- }; +- } else if (done) { +- // async iterable resolver just finished but there are pending payloads +- // return the next one +- return _this._race(); +- } ++ if (index === -1) { ++ // a different call to next has consumed this payload ++ resolve(_this._race()); ++ return; ++ } + +- var returnValue = _objectSpread(_objectSpread({}, value), {}, { +- hasNext: _this._subsequentPayloads.length > 0 +- }); ++ _this._subsequentPayloads.splice(index, 1); + +- return { +- value: returnValue, +- done: false +- }; ++ var value = payload.value, ++ done = payload.done; ++ ++ if (done && _this._subsequentPayloads.length === 0) { ++ // async iterable resolver just finished and no more pending payloads ++ resolve({ ++ value: { ++ hasNext: false, ++ }, ++ done: false, ++ }); ++ return; ++ } else if (done) { ++ // async iterable resolver just finished but there are pending payloads ++ // return the next one ++ resolve(_this._race()); ++ return; ++ } ++ ++ var returnValue = _objectSpread(_objectSpread({}, value), {}, { ++ hasNext: _this._subsequentPayloads.length > 0 ++ }); ++ ++ resolve({ ++ value: returnValue, ++ done: false, ++ }); ++ }); ++ }); + }); + }; + diff --git a/scripts/match-graphql.js b/scripts/match-graphql.js index 04be8f948af..fafcfe63fa2 100644 --- a/scripts/match-graphql.js +++ b/scripts/match-graphql.js @@ -6,13 +6,13 @@ const pkgPath = resolve(cwd(), './package.json'); const pkg = require(pkgPath); -const version = argv[2]; +const versionOrTag = argv[2]; pkg.resolutions = pkg.resolutions || {}; -if (pkg.resolutions.graphql.startsWith(version)){ - console.info(`GraphQL v${version} already installed! Skipping.`) +if (pkg.resolutions.graphql.startsWith(versionOrTag)){ + console.info(`GraphQL v${versionOrTag} already installed! Skipping.`) } -pkg.resolutions.graphql = `^${version}`; +pkg.resolutions.graphql = typeof versionOrTag === 'number' ? `^${versionOrTag}`: versionOrTag; writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), 'utf8'); From 63621459361d7020f192e9ce9fd000c8d5eed9ac Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 21:56:36 +0300 Subject: [PATCH 02/49] add changeset --- .changeset/wild-mangos-deny.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/wild-mangos-deny.md diff --git a/.changeset/wild-mangos-deny.md b/.changeset/wild-mangos-deny.md new file mode 100644 index 00000000000..a6e098b013e --- /dev/null +++ b/.changeset/wild-mangos-deny.md @@ -0,0 +1,12 @@ +--- +'@graphql-tools/delegate': major +'@graphql-tools/mock': major +'@graphql-tools/utils': major +'@graphql-tools/wrap': major +'@graphql-tools/batch-delegate': major +'@graphql-tools/batch-execute': major +'graphql-tools': major +'@graphql-tools/stitch': major +--- + +defer support From 3ba3682f7c92add5760636282b1eac9524173c00 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:02:34 +0300 Subject: [PATCH 03/49] update delegate to support defer/stream - delegateToSchema stores a Receiver object within any returned ExternalObject. The Receiver manages the incoming AsyncIterable payloads and any requests for as yet unretrieved data. - use new type merging algorithm that lazily merges fields from additional subschemas as needed at field resolution time. - add __typename to all selection sets, but only once, needed because any type with deferred fields requires __typename --- packages/delegate/package.json | 3 +- packages/delegate/src/Receiver.ts | 335 +++++++++++++++ packages/delegate/src/Transformer.ts | 4 +- .../delegate/src/defaultMergedResolver.ts | 68 ++- packages/delegate/src/delegateToSchema.ts | 79 +++- packages/delegate/src/delegationBindings.ts | 29 +- packages/delegate/src/expectantStore.ts | 57 +++ packages/delegate/src/externalObjects.ts | 143 ++++--- ...ltAndHandleErrors.ts => externalValues.ts} | 88 ++-- .../delegate/src/getFieldsNotInSubschema.ts | 70 --- packages/delegate/src/getMergedParent.ts | 405 ++++++++++++++++++ packages/delegate/src/index.ts | 2 + packages/delegate/src/mergeFields.ts | 248 ----------- packages/delegate/src/resolveExternalValue.ts | 78 +--- packages/delegate/src/symbols.ts | 3 + .../delegate/src/transforms/AddFieldNodes.ts | 85 ++++ .../src/transforms/AddSelectionSets.ts | 92 ---- ...ddTypenameToAbstract.ts => AddTypename.ts} | 10 +- .../src/transforms/ExpandAbstractTypes.ts | 29 +- .../src/transforms/StoreAsyncSelectionSets.ts | 205 +++++++++ .../src/transforms/VisitSelectionSets.ts | 109 ++--- packages/delegate/src/transforms/index.ts | 6 +- packages/delegate/src/types.ts | 27 +- packages/delegate/tests/defer.test.ts | 329 ++++++++++++++ packages/delegate/tests/errors.test.ts | 24 +- packages/delegate/tests/stream.test.ts | 107 +++++ .../stitch/src/createMergedTypeResolver.ts | 2 - .../tests/alternateStitchSchemas.test.ts | 3 - yarn.lock | 13 +- 29 files changed, 1904 insertions(+), 749 deletions(-) create mode 100644 packages/delegate/src/Receiver.ts create mode 100644 packages/delegate/src/expectantStore.ts rename packages/delegate/src/{transforms/CheckResultAndHandleErrors.ts => externalValues.ts} (56%) delete mode 100644 packages/delegate/src/getFieldsNotInSubschema.ts create mode 100644 packages/delegate/src/getMergedParent.ts delete mode 100644 packages/delegate/src/mergeFields.ts create mode 100644 packages/delegate/src/transforms/AddFieldNodes.ts delete mode 100644 packages/delegate/src/transforms/AddSelectionSets.ts rename packages/delegate/src/transforms/{AddTypenameToAbstract.ts => AddTypename.ts} (76%) create mode 100644 packages/delegate/src/transforms/StoreAsyncSelectionSets.ts create mode 100644 packages/delegate/tests/defer.test.ts create mode 100644 packages/delegate/tests/stream.test.ts diff --git a/packages/delegate/package.json b/packages/delegate/package.json index 66a35adeeee..a298d0e8e50 100644 --- a/packages/delegate/package.json +++ b/packages/delegate/package.json @@ -22,10 +22,11 @@ "input": "./src/index.ts" }, "dependencies": { + "@ardatan/aggregate-error": "0.0.6", "@graphql-tools/batch-execute": "^7.1.2", "@graphql-tools/schema": "^7.1.5", "@graphql-tools/utils": "^7.7.1", - "@ardatan/aggregate-error": "0.0.6", + "@repeaterjs/repeater": "^3.0.4", "dataloader": "2.0.0", "tslib": "~2.2.0", "value-or-promise": "1.0.8" diff --git a/packages/delegate/src/Receiver.ts b/packages/delegate/src/Receiver.ts new file mode 100644 index 00000000000..85242731ba7 --- /dev/null +++ b/packages/delegate/src/Receiver.ts @@ -0,0 +1,335 @@ +import { + ExecutionPatchResult, + ExecutionResult, + getNamedType, + GraphQLList, + GraphQLOutputType, + GraphQLResolveInfo, + isCompositeType, + Kind, + responsePathAsArray, + SelectionSetNode, +} from 'graphql'; + +import DataLoader from 'dataloader'; + +import { Repeater, Stop } from '@repeaterjs/repeater'; + +import { AsyncExecutionResult, getResponseKeyFromInfo } from '@graphql-tools/utils'; + +import { DelegationContext, ExternalObject } from './types'; +import { getReceiver, getSubschema, getUnpathedErrors, mergeExternalObjects } from './externalObjects'; +import { resolveExternalValue } from './resolveExternalValue'; +import { externalValueFromResult, externalValueFromPatchResult } from './externalValues'; +import { ExpectantStore } from './expectantStore'; + +export class Receiver { + private readonly asyncIterable: AsyncIterable; + private readonly delegationContext: DelegationContext; + private readonly fieldName: string; + private readonly context: Record; + private readonly asyncSelectionSets: Record; + private readonly resultTransformer: (originalResult: ExecutionResult) => any; + private readonly initialResultDepth: number; + private deferredPatches: Record>; + private streamedPatches: Record>>; + private cache: ExpectantStore; + private stoppers: Array; + private loaders: Record>; + private infos: Record>; + + constructor( + asyncIterable: AsyncIterable, + delegationContext: DelegationContext, + resultTransformer: (originalResult: ExecutionResult) => any + ) { + this.asyncIterable = asyncIterable; + + this.delegationContext = delegationContext; + const { fieldName, context, info, asyncSelectionSets } = delegationContext; + + this.fieldName = fieldName; + this.context = context; + this.asyncSelectionSets = asyncSelectionSets; + + this.resultTransformer = resultTransformer; + this.initialResultDepth = info ? responsePathAsArray(info.path).length - 1 : 0; + + this.deferredPatches = Object.create(null); + this.streamedPatches = Object.create(null); + this.cache = new ExpectantStore(); + this.stoppers = []; + this.loaders = Object.create(null); + this.infos = Object.create(null); + } + + public async getInitialResult(): Promise { + let initialResult: any; + const payloads: Array = []; + for await (const payload of this.asyncIterable) { + initialResult = externalValueFromResult(this.resultTransformer(payload), this.delegationContext, this); + payloads.push(payload); + if (initialResult != null) { + break; + } + } + this.cache.set(getResponseKeyFromInfo(this.delegationContext.info), initialResult); + + this._iterate(); + + return initialResult; + } + + public request(info: GraphQLResolveInfo): Promise { + const path = responsePathAsArray(info.path).slice(this.initialResultDepth); + const pathKey = path.join('.'); + let loader = this.loaders[pathKey]; + + if (loader === undefined) { + loader = this.loaders[pathKey] = new DataLoader(infos => this._request(path, pathKey, infos)); + } + + return loader.load(info); + } + + private async _request( + path: Array, + pathKey: string, + infos: ReadonlyArray + ): Promise { + const parentPath = path.slice(); + const responseKey = parentPath.pop() as string; + const parentKey = parentPath.join('.'); + + const combinedInfo: GraphQLResolveInfo = { + ...infos[0], + fieldNodes: [].concat(...infos.map(info => info.fieldNodes)), + }; + + let infosByParentKey = this.infos[parentKey]; + if (infosByParentKey === undefined) { + infosByParentKey = this.infos[parentKey] = Object.create(null); + } + + if (infosByParentKey[responseKey] === undefined) { + infosByParentKey[responseKey] = combinedInfo; + this.onNewInfo(pathKey, combinedInfo); + } + + const parent = this.cache.get(parentKey); + + if (parent === undefined) { + throw new Error(`Parent with key "${parentKey}" not available.`) + } + + const data = parent[responseKey]; + if (data !== undefined) { + const unpathedErrors = getUnpathedErrors(parent); + const subschema = getSubschema(parent, responseKey); + const receiver = getReceiver(parent, subschema); + this.onNewExternalValue( + pathKey, + resolveExternalValue(data, unpathedErrors, subschema, this.context, combinedInfo, receiver), + isCompositeType(getNamedType(combinedInfo.returnType)) + ? { + kind: Kind.SELECTION_SET, + selections: [].concat(...combinedInfo.fieldNodes.map(fieldNode => fieldNode.selectionSet.selections)), + } + : undefined + ); + } + + if (fieldShouldStream(combinedInfo)) { + return infos.map( + () => + new Repeater(async (push, stop) => { + const initialValues = ((await this.cache.request(pathKey)) as unknown) as Array; + initialValues.forEach(async value => push(value)); + + let index = initialValues.length; + + let stopped = false; + stop.then(() => (stopped = true)); + + this.stoppers.push(stop); + + const next = () => this.cache.request(`${pathKey}.${index++}`); + + /* eslint-disable no-unmodified-loop-condition */ + while (!stopped) { + await push(next()); + } + /* eslint-disable no-unmodified-loop-condition */ + }) + ); + } + + const externalValue = await this.cache.request(pathKey); + return new Array(infos.length).fill(externalValue); + } + + private async _iterate(): Promise { + const iterator = this.asyncIterable[Symbol.asyncIterator](); + + let hasNext = true; + while (hasNext) { + const payload = (await iterator.next()) as IteratorResult; + + hasNext = !payload.done; + const asyncResult = payload.value; + + if (asyncResult == null) { + continue; + } + + const path = asyncResult.path; + + if (path[0] !== this.fieldName) { + // TODO: throw error? + continue; + } + + const transformedResult = this.resultTransformer(asyncResult); + + if (path.length === 1) { + const newExternalValue = externalValueFromPatchResult( + transformedResult, + this.delegationContext, + this.delegationContext.info, + this + ); + const pathKey = path.join('.'); + this.onNewExternalValue(pathKey, newExternalValue, this.asyncSelectionSets[asyncResult.label]); + continue; + } + + const lastPathSegment = path[path.length - 1]; + const isStreamPatch = typeof lastPathSegment === 'number'; + if (isStreamPatch) { + const parentPath = path.slice(); + const index = parentPath.pop(); + const responseKey = parentPath.pop(); + const parentPathKey = parentPath.join('.'); + const pathKey = `${parentPathKey}.${responseKey}`; + const info = this.infos[parentPathKey]?.[responseKey]; + if (info === undefined) { + const streamedPatches = this.streamedPatches[pathKey]; + if (streamedPatches === undefined) { + this.streamedPatches[pathKey] = { [index]: [transformedResult] }; + continue; + } + + const indexPatches = streamedPatches[index]; + if (indexPatches === undefined) { + streamedPatches[index] = [transformedResult]; + continue; + } + + indexPatches.push(transformedResult); + continue; + } + + const newExternalValue = externalValueFromPatchResult(transformedResult, this.delegationContext, info, this); + this.onNewExternalValue(`${pathKey}.${index}`, newExternalValue, this.asyncSelectionSets[asyncResult.label]); + continue; + } + + const parentPath = path.slice(); + const responseKey = parentPath.pop(); + const parentPathKey = parentPath.join('.'); + const pathKey = `${parentPathKey}.${responseKey}`; + const info = this.infos[parentPathKey]?.[responseKey]; + if (info === undefined) { + const deferredPatches = this.deferredPatches[pathKey]; + if (deferredPatches === undefined) { + this.deferredPatches[pathKey] = [transformedResult]; + continue; + } + + deferredPatches.push(transformedResult); + continue; + } + + const newExternalValue = externalValueFromPatchResult(transformedResult, this.delegationContext, info, this); + this.onNewExternalValue(`${pathKey}`, newExternalValue, this.asyncSelectionSets[asyncResult.label]); + } + + setTimeout(() => { + this.cache.clear(); + this.stoppers.forEach(stop => stop()); + }); + } + + private onNewExternalValue(pathKey: string, newExternalValue: any, selectionSet: SelectionSetNode): void { + const externalValue = this.cache.get(pathKey); + this.cache.set( + pathKey, + externalValue === undefined + ? newExternalValue + : mergeExternalObjects( + this.delegationContext.info.schema, + pathKey.split('.'), + externalValue.__typename, + externalValue, + [newExternalValue], + [selectionSet] + ) + ); + + const infosByParentKey = this.infos[pathKey]; + if (infosByParentKey !== undefined) { + const unpathedErrors = getUnpathedErrors(newExternalValue); + Object.keys(infosByParentKey).forEach(responseKey => { + const info = infosByParentKey[responseKey]; + const data = newExternalValue[responseKey]; + if (data !== undefined) { + const subschema = getSubschema(newExternalValue, responseKey); + const receiver = getReceiver(newExternalValue, subschema); + const subExternalValue = resolveExternalValue(data, unpathedErrors, subschema, this.context, info, receiver); + const subPathKey = `${pathKey}.${responseKey}`; + this.onNewExternalValue( + subPathKey, + subExternalValue, + isCompositeType(getNamedType(info.returnType)) + ? { + kind: Kind.SELECTION_SET, + selections: [].concat(...info.fieldNodes.map(fieldNode => fieldNode.selectionSet.selections)), + } + : undefined + ); + } + }); + } + + this.cache.set(pathKey, newExternalValue); + } + + private onNewInfo(pathKey: string, info: GraphQLResolveInfo): void { + const deferredPatches = this.deferredPatches[pathKey]; + if (deferredPatches !== undefined) { + deferredPatches.forEach(deferredPatch => { + const newExternalValue = externalValueFromPatchResult(deferredPatch, this.delegationContext, info, this); + this.onNewExternalValue(pathKey, newExternalValue, this.asyncSelectionSets[deferredPatch.label]); + }); + } + + const streamedPatches = this.streamedPatches[pathKey]; + if (streamedPatches !== undefined) { + const listMemberInfo: GraphQLResolveInfo = { + ...info, + returnType: (info.returnType as GraphQLList).ofType, + }; + Object.entries(streamedPatches).forEach(([index, indexPatches]) => { + indexPatches.forEach(patch => { + const newExternalValue = externalValueFromPatchResult(patch, this.delegationContext, listMemberInfo, this); + this.onNewExternalValue(`${pathKey}.${index}`, newExternalValue, this.asyncSelectionSets[patch.label]); + }); + }); + } + } +} + +function fieldShouldStream(info: GraphQLResolveInfo): boolean { + const directives = info.fieldNodes[0]?.directives; + return directives !== undefined && directives.some(directive => directive.name.value === 'stream'); +} diff --git a/packages/delegate/src/Transformer.ts b/packages/delegate/src/Transformer.ts index 59bf49a83ec..253448dc908 100644 --- a/packages/delegate/src/Transformer.ts +++ b/packages/delegate/src/Transformer.ts @@ -23,7 +23,7 @@ export class Transformer { this.transformations.push({ transform, context }); } - public transformRequest(originalRequest: Request) { + public transformRequest(originalRequest: Request): Request { return this.transformations.reduce( (request: Request, transformation: Transformation) => transformation.transform.transformRequest != null @@ -33,7 +33,7 @@ export class Transformer { ); } - public transformResult(originalResult: ExecutionResult) { + public transformResult(originalResult: ExecutionResult): any { return this.transformations.reduceRight( (result: ExecutionResult, transformation: Transformation) => transformation.transform.transformResult != null diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index 8ae4e9ce24d..25a8bbb4e36 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -1,38 +1,78 @@ -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 './externalObjects'; import { ExternalObject } from './types'; +import { resolveExternalValue } from './resolveExternalValue'; +import { + getInitialPossibleFields, + getReceiver, + 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/apollographql/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) { + const unpathedErrors = getUnpathedErrors(parent); + const fieldSubschema = getSubschema(parent, responseKey); + return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info); + } + } else if (info.fieldNodes[0].name.value in initialPossibleFields) { + return resolveField(parent, responseKey, context, info); + } + + return getMergedParent(parent, context, info).then(mergedParent => + resolveField(mergedParent, responseKey, context, info) + ); +} + +function resolveField( + parent: ExternalObject, + responseKey: string, + context: Record, + info: GraphQLResolveInfo +): any { + const fieldSubschema = getSubschema(parent, responseKey); + const receiver = getReceiver(parent, fieldSubschema); + + if (receiver !== undefined) { + return receiver.request(info); } const data = parent[responseKey]; - const unpathedErrors = getUnpathedErrors(parent); - const subschema = getSubschema(parent, responseKey); + if (data !== undefined) { + const unpathedErrors = getUnpathedErrors(parent); + return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info, receiver); + } - return resolveExternalValue(data, unpathedErrors, subschema, context, info); + // throw error? } diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index a120ae46db1..46b30f47982 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -18,15 +18,30 @@ import AggregateError from '@ardatan/aggregate-error'; import { getBatchingExecutor } from '@graphql-tools/batch-execute'; -import { mapAsyncIterator, ExecutionResult, Executor, ExecutionParams, Subscriber } from '@graphql-tools/utils'; +import { + AsyncExecutionResult, + ExecutionParams, + ExecutionResult, + Executor, + isAsyncIterable, + mapAsyncIterator, + Subscriber, +} from '@graphql-tools/utils'; -import { IDelegateToSchemaOptions, IDelegateRequestOptions, StitchingInfo, DelegationContext } from './types'; +import { + DelegationContext, + IDelegateToSchemaOptions, + IDelegateRequestOptions, + StitchingInfo, +} from './types'; import { isSubschemaConfig } from './subschemaConfig'; import { Subschema } from './Subschema'; import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; import { Transformer } from './Transformer'; import { memoize2 } from './memoize'; +import { Receiver } from './Receiver'; +import { externalValueFromResult } from './externalValues'; export function delegateToSchema, TArgs = any>( options: IDelegateToSchemaOptions @@ -95,8 +110,14 @@ export function delegateRequest, TArgs = any>(opt return new ValueOrPromise(() => executor({ ...processedRequest, context, - info, - })).then(originalResult => transformer.transformResult(originalResult)).resolve(); + info + })).then( + executionResult => handleExecutionResult( + executionResult, + delegationContext, + originalResult => transformer.transformResult(originalResult) + ) + ).resolve(); } const subscriber = getSubscriber(delegationContext); @@ -105,19 +126,40 @@ export function delegateRequest, TArgs = any>(opt ...processedRequest, context, info, - }).then((subscriptionResult: AsyncIterableIterator | ExecutionResult) => { - if (Symbol.asyncIterator in subscriptionResult) { - // "subscribe" to the subscription result and map the result through the transforms - return mapAsyncIterator( - subscriptionResult as AsyncIterableIterator, - originalResult => ({ - [delegationContext.fieldName]: transformer.transformResult(originalResult), - }) - ); - } + }).then((subscriptionResult: AsyncIterableIterator | ExecutionResult) => + handleSubscriptionResult(subscriptionResult, delegationContext, originalResult => + transformer.transformResult(originalResult) + ) + ); +} - return transformer.transformResult(subscriptionResult as ExecutionResult); - }); +function handleExecutionResult( + executionResult: ExecutionResult | AsyncIterableIterator, + delegationContext: DelegationContext, + resultTransformer: (originalResult: ExecutionResult) => ExecutionResult +): any { + if (isAsyncIterable(executionResult)) { + const receiver = new Receiver(executionResult, delegationContext, resultTransformer); + + return receiver.getInitialResult(); + } + + return externalValueFromResult(resultTransformer(executionResult), delegationContext); +} + +function handleSubscriptionResult( + subscriptionResult: AsyncIterableIterator | ExecutionResult, + delegationContext: DelegationContext, + resultTransformer: (originalResult: ExecutionResult) => any +): ExecutionResult | AsyncIterableIterator { + if (isAsyncIterable(subscriptionResult)) { + // "subscribe" to the subscription result and map the result through the transforms + return mapAsyncIterator(subscriptionResult, originalResult => ({ + [delegationContext.fieldName]: externalValueFromResult(resultTransformer(originalResult), delegationContext), + })); + } + + return resultTransformer(subscriptionResult); } const emptyObject = {}; @@ -134,7 +176,6 @@ function getDelegationContext({ rootValue, transforms = [], transformedSchema, - skipTypeMerging, }: IDelegateRequestOptions): DelegationContext { let operationDefinition: OperationDefinitionNode; let targetOperation: OperationTypeNode; @@ -176,7 +217,7 @@ function getDelegationContext({ ? subschemaOrSubschemaConfig.transforms.concat(transforms) : transforms, transformedSchema: transformedSchema ?? (subschemaOrSubschemaConfig as Subschema)?.transformedSchema ?? targetSchema, - skipTypeMerging, + asyncSelectionSets: Object.create(null), }; } @@ -193,7 +234,7 @@ function getDelegationContext({ returnType: returnType ?? info?.returnType ?? getDelegationReturnType(subschemaOrSubschemaConfig, targetOperation, targetFieldName), transforms, transformedSchema: transformedSchema ?? subschemaOrSubschemaConfig, - skipTypeMerging, + asyncSelectionSets: Object.create(null), }; } diff --git a/packages/delegate/src/delegationBindings.ts b/packages/delegate/src/delegationBindings.ts index d5946f1f94f..c3ff577090a 100644 --- a/packages/delegate/src/delegationBindings.ts +++ b/packages/delegate/src/delegationBindings.ts @@ -1,38 +1,33 @@ import { Transform, StitchingInfo, DelegationContext } from './types'; -import AddSelectionSets from './transforms/AddSelectionSets'; +import AddFieldNodes from './transforms/AddFieldNodes'; import ExpandAbstractTypes from './transforms/ExpandAbstractTypes'; import WrapConcreteTypes from './transforms/WrapConcreteTypes'; import FilterToSchema from './transforms/FilterToSchema'; -import AddTypenameToAbstract from './transforms/AddTypenameToAbstract'; -import CheckResultAndHandleErrors from './transforms/CheckResultAndHandleErrors'; +import AddTypename from './transforms/AddTypename'; import AddArgumentsAsVariables from './transforms/AddArgumentsAsVariables'; +import StoreAsyncSelectionSets from './transforms/StoreAsyncSelectionSets'; export function defaultDelegationBinding(delegationContext: DelegationContext): Array { - let delegationTransforms: Array = [new CheckResultAndHandleErrors()]; + const delegationTransforms: Array = []; const info = delegationContext.info; const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; if (stitchingInfo != null) { - delegationTransforms = delegationTransforms.concat([ + delegationTransforms.push( new ExpandAbstractTypes(), - new AddSelectionSets( - stitchingInfo.selectionSetsByType, - stitchingInfo.selectionSetsByField, - stitchingInfo.dynamicSelectionSetsByField - ), - new WrapConcreteTypes(), - ]); + new AddFieldNodes(stitchingInfo.fieldNodesByField, stitchingInfo.dynamicFieldNodesByField) + ); } else if (info != null) { - delegationTransforms = delegationTransforms.concat([new WrapConcreteTypes(), new ExpandAbstractTypes()]); - } else { - delegationTransforms.push(new WrapConcreteTypes()); + delegationTransforms.push(new ExpandAbstractTypes()); } + delegationTransforms.push(new WrapConcreteTypes(), new StoreAsyncSelectionSets()); + const transforms = delegationContext.transforms; if (transforms != null) { - delegationTransforms = delegationTransforms.concat(transforms.slice().reverse()); + delegationTransforms.push(...transforms.slice().reverse()); } const args = delegationContext.args; @@ -40,7 +35,7 @@ export function defaultDelegationBinding(delegationContext: DelegationContext): delegationTransforms.push(new AddArgumentsAsVariables(args)); } - delegationTransforms = delegationTransforms.concat([new FilterToSchema(), new AddTypenameToAbstract()]); + delegationTransforms.push(new AddTypename(), new FilterToSchema()); return delegationTransforms; } diff --git a/packages/delegate/src/expectantStore.ts b/packages/delegate/src/expectantStore.ts new file mode 100644 index 00000000000..109b29c2e12 --- /dev/null +++ b/packages/delegate/src/expectantStore.ts @@ -0,0 +1,57 @@ +interface Settler { + resolve(value: T): void; + reject(reason?: any): void; +} + +export class ExpectantStore { + protected settlers: Record>> = {}; + protected cache: Record = {}; + + set(key: string, value: T): void { + if (Array.isArray(value)) { + value.forEach((v, index) => this.set(`${key}.${index}`, v)); + } + + this.cache[key] = value; + const settlers = this.settlers[key]; + if (settlers != null) { + for (const { resolve } of settlers) { + resolve(value); + } + settlers.clear(); + delete this.settlers[key]; + } + } + + get(key: string): T { + return this.cache[key]; + } + + request(key: string): Promise | T { + const value = this.cache[key]; + + if (value !== undefined) { + return value; + } + + let settlers = this.settlers[key]; + if (settlers === undefined) { + settlers = this.settlers[key] = new Set(); + } + + return new Promise((resolve, reject) => { + settlers.add({ resolve, reject }); + }); + } + + clear(): void { + for (const [key, settlers] of Object.entries(this.settlers)) { + for (const { reject } of settlers) { + reject(`"${key}" requested, but never provided.`); + } + } + + this.settlers = {}; + this.cache = {}; + } +} diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts index 631afc45114..e8375eda74f 100644 --- a/packages/delegate/src/externalObjects.ts +++ b/packages/delegate/src/externalObjects.ts @@ -1,98 +1,145 @@ -import { GraphQLSchema, GraphQLError, GraphQLObjectType, SelectionSetNode, locatedError } from 'graphql'; +import { + GraphQLSchema, + GraphQLError, + GraphQLFieldMap, + GraphQLObjectType, + GraphQLResolveInfo, + SelectionSetNode, + locatedError, +} from 'graphql'; -import { mergeDeep, relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils'; +import { relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils'; import { SubschemaConfig, ExternalObject } from './types'; -import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; +import { + OBJECT_SUBSCHEMA_SYMBOL, + INITIAL_POSSIBLE_FIELDS, + INFO_SYMBOL, + FIELD_SUBSCHEMA_MAP_SYMBOL, + UNPATHED_ERRORS_SYMBOL, + RECEIVER_MAP_SYMBOL, +} from './symbols'; +import { Receiver } from './Receiver'; +import { isSubschemaConfig } from './subschemaConfig'; +import { Subschema } from './Subschema'; export function isExternalObject(data: any): data is ExternalObject { - return data[UNPATHED_ERRORS_SYMBOL] !== undefined; + return data != null && data[UNPATHED_ERRORS_SYMBOL] !== undefined; } export function annotateExternalObject( object: any, errors: Array, - subschema: GraphQLSchema | SubschemaConfig + subschema: GraphQLSchema | SubschemaConfig, + info: GraphQLResolveInfo, + receiver: Receiver ): ExternalObject { + const schema = isSubschemaConfig(subschema) + ? (subschema as Subschema)?.transformedSchema ?? subschema.schema + : subschema; + + const initialPossibleFields = (schema.getType(object.__typename) as GraphQLObjectType)?.getFields(); + + const receiverMap: Map = new Map(); + receiverMap.set(subschema, receiver); + Object.defineProperties(object, { [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, + [INITIAL_POSSIBLE_FIELDS]: { value: initialPossibleFields }, + [INFO_SYMBOL]: { value: info }, [FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: Object.create(null) }, [UNPATHED_ERRORS_SYMBOL]: { value: errors }, + [RECEIVER_MAP_SYMBOL]: { value: receiverMap }, }); return object; } -export function getSubschema(object: ExternalObject, responseKey: string): GraphQLSchema | SubschemaConfig { - return object[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? object[OBJECT_SUBSCHEMA_SYMBOL]; +export function getSubschema(object: ExternalObject, responseKey?: string): GraphQLSchema | SubschemaConfig { + return responseKey === undefined + ? object[OBJECT_SUBSCHEMA_SYMBOL] + : object[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? object[OBJECT_SUBSCHEMA_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 getReceiver(object: ExternalObject, subschema: GraphQLSchema | SubschemaConfig): Receiver { + return object[RECEIVER_MAP_SYMBOL].get(subschema); +} + export function mergeExternalObjects( schema: GraphQLSchema, - path: Array, + path: ReadonlyArray, typeName: string, target: ExternalObject, sources: Array, selectionSets: Array ): ExternalObject { - const results: Array = []; - let errors: Array = []; + if (target[FIELD_SUBSCHEMA_MAP_SYMBOL] == null) { + target[FIELD_SUBSCHEMA_MAP_SYMBOL] = Object.create(null); + } + + const newFieldSubschemaMap = target[FIELD_SUBSCHEMA_MAP_SYMBOL]; + const newReceiverMap = target[RECEIVER_MAP_SYMBOL]; + const newUnpathedErrors = target[UNPATHED_ERRORS_SYMBOL]; sources.forEach((source, index) => { + const fieldNodes = collectFields( + { + schema, + variableValues: {}, + fragments: {}, + } as GraphQLExecutionContext, + schema.getType(typeName) as GraphQLObjectType, + selectionSets[index], + Object.create(null), + Object.create(null) + ); + if (source instanceof Error || source === null) { - const selectionSet = selectionSets[index]; - const fieldNodes = collectFields( - { - schema, - variableValues: {}, - fragments: {}, - } as GraphQLExecutionContext, - schema.getType(typeName) as GraphQLObjectType, - selectionSet, - Object.create(null), - Object.create(null) - ); - const nullResult = {}; Object.keys(fieldNodes).forEach(responseKey => { if (source instanceof GraphQLError) { - nullResult[responseKey] = relocatedError(source, path.concat([responseKey])); + target[responseKey] = relocatedError(source, path.concat([responseKey])); } else if (source instanceof Error) { - nullResult[responseKey] = locatedError(source, fieldNodes[responseKey], path.concat([responseKey])); + target[responseKey] = locatedError(source, fieldNodes[responseKey], path.concat([responseKey])); } else { - nullResult[responseKey] = null; + target[responseKey] = null; } }); - results.push(nullResult); - } else { - errors = errors.concat(source[UNPATHED_ERRORS_SYMBOL]); - results.push(source); + return; } - }); - - const combinedResult: ExternalObject = results.reduce(mergeDeep, target); - const newFieldSubschemaMap = target[FIELD_SUBSCHEMA_MAP_SYMBOL] ?? Object.create(null); - - results.forEach((source: ExternalObject) => { const objectSubschema = source[OBJECT_SUBSCHEMA_SYMBOL]; - const fieldSubschemaMap = source[FIELD_SUBSCHEMA_MAP_SYMBOL]; - if (fieldSubschemaMap === undefined) { - Object.keys(source).forEach(responseKey => { - newFieldSubschemaMap[responseKey] = objectSubschema; + Object.keys(fieldNodes).forEach(responseKey => { + target[responseKey] = source[responseKey]; + newFieldSubschemaMap[responseKey] = objectSubschema; + }); + + if (isExternalObject(source)) { + const receiverMap = source[RECEIVER_MAP_SYMBOL]; + receiverMap.forEach((receiver, subschema) => { + if (receiver) { + newReceiverMap.set(subschema, receiver); + } }); - } else { - Object.keys(source).forEach(responseKey => { - newFieldSubschemaMap[responseKey] = fieldSubschemaMap[responseKey] ?? objectSubschema; + + newUnpathedErrors.push(...source[UNPATHED_ERRORS_SYMBOL]); + + const fieldSubschemaMap = source[FIELD_SUBSCHEMA_MAP_SYMBOL]; + Object.keys(fieldSubschemaMap).forEach(responseKey => { + newFieldSubschemaMap[responseKey] = fieldSubschemaMap[responseKey]; }); } }); - combinedResult[FIELD_SUBSCHEMA_MAP_SYMBOL] = newFieldSubschemaMap; - combinedResult[OBJECT_SUBSCHEMA_SYMBOL] = target[OBJECT_SUBSCHEMA_SYMBOL]; - combinedResult[UNPATHED_ERRORS_SYMBOL] = target[UNPATHED_ERRORS_SYMBOL].concat(errors); - - return combinedResult; + return target; } diff --git a/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts b/packages/delegate/src/externalValues.ts similarity index 56% rename from packages/delegate/src/transforms/CheckResultAndHandleErrors.ts rename to packages/delegate/src/externalValues.ts index 264541de177..1c7e608e77d 100644 --- a/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts +++ b/packages/delegate/src/externalValues.ts @@ -1,56 +1,60 @@ -import { - GraphQLResolveInfo, - GraphQLOutputType, - GraphQLSchema, - GraphQLError, - responsePathAsArray, - locatedError, -} from 'graphql'; +import { GraphQLError, responsePathAsArray, locatedError, GraphQLResolveInfo } from 'graphql'; import AggregateError from '@ardatan/aggregate-error'; -import { getResponseKeyFromInfo, ExecutionResult, relocatedError } from '@graphql-tools/utils'; - -import { SubschemaConfig, Transform, DelegationContext } from '../types'; -import { resolveExternalValue } from '../resolveExternalValue'; - -export default class CheckResultAndHandleErrors implements Transform { - public transformResult( - originalResult: ExecutionResult, - delegationContext: DelegationContext, - _transformationContext: Record - ): ExecutionResult { - return checkResultAndHandleErrors( - originalResult, - delegationContext.context != null ? delegationContext.context : {}, - delegationContext.info, - delegationContext.fieldName, - delegationContext.subschema, - delegationContext.returnType, - delegationContext.skipTypeMerging, - delegationContext.onLocatedError - ); - } +import { ExecutionPatchResult, ExecutionResult, relocatedError } from '@graphql-tools/utils'; + +import { DelegationContext } from './types'; +import { resolveExternalValue } from './resolveExternalValue'; +import { Receiver } from './Receiver'; + +export function externalValueFromResult( + originalResult: ExecutionResult, + delegationContext: DelegationContext, + receiver?: Receiver +): any { + return externalValueFromDataAndErrors( + originalResult.data?.[delegationContext.fieldName], + originalResult.errors ?? [], + delegationContext, + receiver + ); } -export function checkResultAndHandleErrors( - result: ExecutionResult, - context: Record, +export function externalValueFromPatchResult( + originalResult: ExecutionPatchResult, + delegationContext: DelegationContext, info: GraphQLResolveInfo, - responseKey: string = getResponseKeyFromInfo(info), - subschema?: GraphQLSchema | SubschemaConfig, - returnType: GraphQLOutputType = info.returnType, - skipTypeMerging?: boolean, - onLocatedError?: (originalError: GraphQLError) => GraphQLError + receiver: Receiver +): any { + return externalValueFromDataAndErrors( + originalResult.data, + originalResult.errors ?? [], + { + ...delegationContext, + info, + returnType: info.returnType, + }, + receiver + ); +} + +function externalValueFromDataAndErrors( + data: any, + errors: ReadonlyArray, + delegationContext: DelegationContext, + receiver?: Receiver ): any { - const { data, unpathedErrors } = mergeDataAndErrors( - result.data == null ? undefined : result.data[responseKey], - result.errors == null ? [] : result.errors, + const { context, subschema, onLocatedError, returnType, info } = delegationContext; + + const { data: newData, unpathedErrors } = mergeDataAndErrors( + data, + errors, info ? responsePathAsArray(info.path) : undefined, onLocatedError ); - return resolveExternalValue(data, unpathedErrors, subschema, context, info, returnType, skipTypeMerging); + return resolveExternalValue(newData, unpathedErrors, subschema, context, info, receiver, returnType); } export function mergeDataAndErrors( diff --git a/packages/delegate/src/getFieldsNotInSubschema.ts b/packages/delegate/src/getFieldsNotInSubschema.ts deleted file mode 100644 index c3a2249304a..00000000000 --- a/packages/delegate/src/getFieldsNotInSubschema.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { GraphQLSchema, FieldNode, GraphQLObjectType, GraphQLResolveInfo } from 'graphql'; - -import { collectFields, GraphQLExecutionContext } from '@graphql-tools/utils'; - -import { isSubschemaConfig } from './subschemaConfig'; -import { MergedTypeInfo, SubschemaConfig, StitchingInfo } from './types'; -import { memoizeInfoAnd2Objects } from './memoize'; - -function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record> { - let subFieldNodes: Record> = Object.create(null); - const visitedFragmentNames = Object.create(null); - - const type = info.schema.getType(typeName) as GraphQLObjectType; - const partialExecutionContext = ({ - schema: info.schema, - variableValues: info.variableValues, - fragments: info.fragments, - } as unknown) as GraphQLExecutionContext; - - info.fieldNodes.forEach(fieldNode => { - subFieldNodes = collectFields( - partialExecutionContext, - type, - fieldNode.selectionSet, - subFieldNodes, - visitedFragmentNames - ); - }); - - const stitchingInfo = info.schema.extensions.stitchingInfo as StitchingInfo; - const selectionSetsByField = stitchingInfo.selectionSetsByField; - - Object.keys(subFieldNodes).forEach(responseName => { - const fieldName = subFieldNodes[responseName][0].name.value; - const fieldSelectionSet = selectionSetsByField?.[typeName]?.[fieldName]; - if (fieldSelectionSet != null) { - subFieldNodes = collectFields( - partialExecutionContext, - type, - fieldSelectionSet, - subFieldNodes, - visitedFragmentNames - ); - } - }); - - return subFieldNodes; -} - -export const getFieldsNotInSubschema = memoizeInfoAnd2Objects(function ( - info: GraphQLResolveInfo, - subschema: GraphQLSchema | SubschemaConfig, - mergedTypeInfo: MergedTypeInfo -): Array { - const typeMap = isSubschemaConfig(subschema) ? mergedTypeInfo.typeMaps.get(subschema) : subschema.getTypeMap(); - const typeName = mergedTypeInfo.typeName; - const fields = (typeMap[typeName] as GraphQLObjectType).getFields(); - - const subFieldNodes = collectSubFields(info, typeName); - - let fieldsNotInSchema: Array = []; - Object.keys(subFieldNodes).forEach(responseName => { - const fieldName = subFieldNodes[responseName][0].name.value; - if (!(fieldName in fields)) { - fieldsNotInSchema = fieldsNotInSchema.concat(subFieldNodes[responseName]); - } - }); - - return fieldsNotInSchema; -}); diff --git a/packages/delegate/src/getMergedParent.ts b/packages/delegate/src/getMergedParent.ts new file mode 100644 index 00000000000..2ac06d1485b --- /dev/null +++ b/packages/delegate/src/getMergedParent.ts @@ -0,0 +1,405 @@ +import { + FieldNode, + GraphQLObjectType, + GraphQLResolveInfo, + Kind, + SelectionSetNode, + responsePathAsArray, + getNamedType, + print, + GraphQLFieldMap, +} from 'graphql'; + +import isPromise from 'is-promise'; + +import DataLoader from 'dataloader'; + +import { getResponseKeyFromInfo } from '@graphql-tools/utils'; + +import { ExternalObject, MergedTypeInfo, StitchingInfo } from './types'; +import { getInfo, getSubschema, mergeExternalObjects } from './externalObjects'; +import { memoize4, memoize3, memoize2 } from './memoize'; +import { Subschema } from './Subschema'; +import { Repeater } from '@repeaterjs/repeater'; + +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 parentInfo = getInfo(parent); + + const schema = parentInfo.schema; + const stitchingInfo: StitchingInfo = schema.extensions?.stitchingInfo; + const parentTypeName = infos[0].parentType.name; + const mergedTypeInfo = stitchingInfo?.mergedTypes[parentTypeName]; + if (mergedTypeInfo === undefined) { + return infos.map(() => Promise.resolve(parent)); + } + + // In the stitching context, all subschemas are compiled Subschema objects rather than SubschemaConfig objects + const sourceSubschema = getSubschema(parent) as Subschema; + const targetSubschemas = mergedTypeInfo.targetSubschemas.get(sourceSubschema); + if (targetSubschemas === undefined || targetSubschemas.length === 0) { + return infos.map(() => Promise.resolve(parent)); + } + + const sourceSubschemaParentType = sourceSubschema.transformedSchema.getType(parentTypeName) as GraphQLObjectType; + const sourceSubschemaFields = sourceSubschemaParentType.getFields(); + const subschemaFields = mergedTypeInfo.subschemaFields; + const keyResponseKeys: Record> = Object.create(null); + const keyFieldNodeMap: Map = new Map(); + const typeFieldNodes = stitchingInfo?.fieldNodesByField?.[parentTypeName]; + const typeDynamicFieldNodes = stitchingInfo?.dynamicFieldNodesByField?.[parentTypeName]; + + let fieldNodes: Array = []; + infos.forEach(info => { + const responseKey = getResponseKeyFromInfo(info); + const fieldName = info.fieldName; + if (subschemaFields[fieldName] !== undefined) { + fieldNodes.push(...info.fieldNodes); + } else { + if (keyResponseKeys[responseKey] === undefined) { + keyResponseKeys[responseKey] = new Set(); + } + } + + const keyFieldNodes: Array = []; + + const fieldNodesByField = typeFieldNodes?.[fieldName]; + if (fieldNodesByField !== undefined) { + keyFieldNodes.push(...fieldNodesByField); + } + + const dynamicFieldNodesByField = typeDynamicFieldNodes?.[fieldName]; + if (dynamicFieldNodesByField !== undefined) { + info.fieldNodes.forEach(fieldNode => { + dynamicFieldNodesByField.forEach(fieldNodeFn => { + keyFieldNodes.push(...fieldNodeFn(fieldNode)); + }); + }); + } + + if (keyResponseKeys[responseKey] !== undefined) { + keyFieldNodes.forEach(fieldNode => { + const keyResponseKey = fieldNode.alias?.value ?? fieldNode.name.value; + keyResponseKeys[responseKey].add(keyResponseKey); + }); + } + addFieldNodesToMap(keyFieldNodeMap, sourceSubschemaFields, keyFieldNodes); + }); + + fieldNodes = fieldNodes.concat(...Array.from(keyFieldNodeMap.values())); + + const mergedParents = getMergedParentsFromFieldNodes( + mergedTypeInfo, + parent, + fieldNodes, + sourceSubschema, + targetSubschemas, + context, + parentInfo + ); + + return infos.map(info => { + const responseKey = getResponseKeyFromInfo(info); + if (keyResponseKeys[responseKey] === undefined) { + return mergedParents[responseKey]; + } + + return slowRace(Array.from(keyResponseKeys[responseKey].values()).map(keyResponseKey => mergedParents[keyResponseKey])); + }); +} + +function getMergedParentsFromFieldNodes( + mergedTypeInfo: MergedTypeInfo, + object: any, + fieldNodes: Array, + sourceSubschemaOrSourceSubschemas: Subschema | Array, + targetSubschemas: Array, + context: Record, + info: GraphQLResolveInfo +): Record> { + if (!fieldNodes.length) { + return Object.create(null); + } + + const { proxiableSubschemas, nonProxiableSubschemas } = sortSubschemasByProxiability( + mergedTypeInfo, + sourceSubschemaOrSourceSubschemas, + targetSubschemas, + fieldNodes + ); + + const { delegationMap, unproxiableFieldNodes } = buildDelegationPlan(mergedTypeInfo, fieldNodes, proxiableSubschemas); + + if (!delegationMap.size) { + const mergedParentMap = Object.create(null); + unproxiableFieldNodes.forEach(fieldNode => { + const responseKey = fieldNode.alias?.value ?? fieldNode.name.value; + mergedParentMap[responseKey] = Promise.resolve(object); + }); + return mergedParentMap; + } + + const resultMap: Map | any, SelectionSetNode> = new Map(); + + const mergedParentMap = Object.create(null); + delegationMap.forEach((fieldNodes: Array, s: Subschema) => { + const resolver = mergedTypeInfo.resolvers.get(s); + const selectionSet = { kind: Kind.SELECTION_SET, selections: fieldNodes }; + let maybePromise = resolver(object, context, info, s, selectionSet); + if (isPromise(maybePromise)) { + maybePromise = maybePromise.then(undefined, error => error); + } + resultMap.set(maybePromise, selectionSet); + + const promise = Promise.resolve(maybePromise).then(result => + mergeExternalObjects( + info.schema, + responsePathAsArray(info.path), + object.__typename, + object, + [result], + [selectionSet] + ) + ); + + fieldNodes.forEach(fieldNode => { + const responseKey = fieldNode.alias?.value ?? fieldNode.name.value; + mergedParentMap[responseKey] = promise; + }); + }); + + const nextPromise = Promise.all(resultMap.keys()) + .then(results => getMergedParentsFromFieldNodes( + mergedTypeInfo, + mergeExternalObjects( + info.schema, + responsePathAsArray(info.path), + object.__typename, + object, + results, + Array.from(resultMap.values()) + ), + unproxiableFieldNodes, + combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), + nonProxiableSubschemas, + context, + info + ) + ); + + unproxiableFieldNodes.forEach(fieldNode => { + const responseKey = fieldNode.alias?.value ?? fieldNode.name.value; + mergedParentMap[responseKey] = nextPromise.then(nextParent => nextParent[responseKey]); + }); + + return mergedParentMap; +} + +const sortSubschemasByProxiability = memoize4(function ( + mergedTypeInfo: MergedTypeInfo, + sourceSubschemaOrSourceSubschemas: Subschema | Array, + targetSubschemas: Array, + fieldNodes: Array +): { + proxiableSubschemas: Array; + nonProxiableSubschemas: Array; +} { + // 1. calculate if possible to delegate to given subschema + + const proxiableSubschemas: Array = []; + const nonProxiableSubschemas: Array = []; + + targetSubschemas.forEach(t => { + const selectionSet = mergedTypeInfo.selectionSets.get(t); + const fieldSelectionSets = mergedTypeInfo.fieldSelectionSets.get(t); + if ( + selectionSet != null && + !subschemaTypesContainSelectionSet(mergedTypeInfo, sourceSubschemaOrSourceSubschemas, 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, sourceSubschemaOrSourceSubschemas, fieldSelectionSet) + ); + }) + ) { + proxiableSubschemas.push(t); + } else { + nonProxiableSubschemas.push(t); + } + } + }); + + return { + proxiableSubschemas, + nonProxiableSubschemas, + }; +}); + +const buildDelegationPlan = memoize3(function ( + mergedTypeInfo: MergedTypeInfo, + fieldNodes: Array, + proxiableSubschemas: Array +): { + delegationMap: Map>; + unproxiableFieldNodes: Array; +} { + const { uniqueFields, nonUniqueFields } = mergedTypeInfo; + const unproxiableFieldNodes: Array = []; + + // 2. for each selection: + + const delegationMap: Map> = new Map(); + fieldNodes.forEach(fieldNode => { + if (fieldNode.name.value === '__typename') { + return; + } + + // 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); + return; + } + + const existingSubschema = delegationMap.get(uniqueSubschema); + if (existingSubschema != null) { + existingSubschema.push(fieldNode); + } else { + delegationMap.set(uniqueSubschema, [fieldNode]); + } + + return; + } + + // 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); + return; + } + + nonUniqueSubschemas = nonUniqueSubschemas.filter(s => proxiableSubschemas.includes(s)); + if (!nonUniqueSubschemas.length) { + unproxiableFieldNodes.push(fieldNode); + return; + } + + const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); + if (existingSubschema != null) { + delegationMap.get(existingSubschema).push(fieldNode); + } else { + delegationMap.set(nonUniqueSubschemas[0], [fieldNode]); + } + }); + + return { + delegationMap, + unproxiableFieldNodes, + }; +}); + +const combineSubschemas = memoize2(function ( + subschemaOrSubschemas: Subschema | Array, + additionalSubschemas: Array +): Array { + return Array.isArray(subschemaOrSubschemas) + ? subschemaOrSubschemas.concat(additionalSubschemas) + : [subschemaOrSubschemas].concat(additionalSubschemas); +}); + +const subschemaTypesContainSelectionSet = memoize3(function ( + mergedTypeInfo: MergedTypeInfo, + sourceSubschemaOrSourceSubschemas: Subschema | Array, + selectionSet: SelectionSetNode +) { + if (Array.isArray(sourceSubschemaOrSourceSubschemas)) { + return typesContainSelectionSet( + sourceSubschemaOrSourceSubschemas.map( + sourceSubschema => sourceSubschema.transformedSchema.getType(mergedTypeInfo.typeName) as GraphQLObjectType + ), + selectionSet + ); + } + + return typesContainSelectionSet( + [sourceSubschemaOrSourceSubschemas.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; +} + +function addFieldNodesToMap( + map: Map, + fields: GraphQLFieldMap, + fieldNodes: Array, +): void { + fieldNodes.forEach(fieldNode => { + const fieldName = fieldNode.name.value; + if (!(fieldName in fields)) { + const key = print(fieldNode); + if (!map.has(key)) { + map.set(key, fieldNode); + } + } + }); +} + +async function slowRace(promises: Array>): Promise { + let last: T; + for await (const result of Repeater.merge(promises)) { + last = result; + } + return last; +} diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 93b3a21b408..20c2153a7ea 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -5,6 +5,8 @@ export * from './defaultMergedResolver'; export * from './delegateToSchema'; export * from './delegationBindings'; export * from './externalObjects'; +export * from './externalValues'; +export * from './getMergedParent'; export * from './resolveExternalValue'; export * from './subschemaConfig'; export * from './transforms'; diff --git a/packages/delegate/src/mergeFields.ts b/packages/delegate/src/mergeFields.ts deleted file mode 100644 index 3b44f3fb867..00000000000 --- a/packages/delegate/src/mergeFields.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { - FieldNode, - SelectionNode, - Kind, - GraphQLResolveInfo, - SelectionSetNode, - GraphQLObjectType, - responsePathAsArray, - getNamedType, -} from 'graphql'; - -import { ValueOrPromise } from 'value-or-promise'; - -import { MergedTypeInfo } from './types'; -import { memoize4, memoize3, memoize2 } from './memoize'; -import { mergeExternalObjects } from './externalObjects'; -import { Subschema } from './Subschema'; - -const sortSubschemasByProxiability = memoize4(function ( - mergedTypeInfo: MergedTypeInfo, - sourceSubschemaOrSourceSubschemas: Subschema | Array, - targetSubschemas: Array, - fieldNodes: Array -): { - proxiableSubschemas: Array; - nonProxiableSubschemas: Array; -} { - // 1. calculate if possible to delegate to given subschema - - const proxiableSubschemas: Array = []; - const nonProxiableSubschemas: Array = []; - - targetSubschemas.forEach(t => { - const selectionSet = mergedTypeInfo.selectionSets.get(t); - const fieldSelectionSets = mergedTypeInfo.fieldSelectionSets.get(t); - if ( - selectionSet != null && - !subschemaTypesContainSelectionSet(mergedTypeInfo, sourceSubschemaOrSourceSubschemas, 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, sourceSubschemaOrSourceSubschemas, fieldSelectionSet) - ); - }) - ) { - proxiableSubschemas.push(t); - } else { - nonProxiableSubschemas.push(t); - } - } - }); - - return { - proxiableSubschemas, - nonProxiableSubschemas, - }; -}); - -const buildDelegationPlan = memoize3(function ( - mergedTypeInfo: MergedTypeInfo, - fieldNodes: Array, - proxiableSubschemas: Array -): { - delegationMap: Map; - unproxiableFieldNodes: Array; -} { - const { uniqueFields, nonUniqueFields } = mergedTypeInfo; - const unproxiableFieldNodes: Array = []; - - // 2. for each selection: - - const delegationMap: Map> = new Map(); - fieldNodes.forEach(fieldNode => { - if (fieldNode.name.value === '__typename') { - return; - } - - // 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); - return; - } - - const existingSubschema = delegationMap.get(uniqueSubschema); - if (existingSubschema != null) { - existingSubschema.push(fieldNode); - } else { - delegationMap.set(uniqueSubschema, [fieldNode]); - } - - return; - } - - // 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); - return; - } - - nonUniqueSubschemas = nonUniqueSubschemas.filter(s => proxiableSubschemas.includes(s)); - if (!nonUniqueSubschemas.length) { - unproxiableFieldNodes.push(fieldNode); - return; - } - - const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); - if (existingSubschema != null) { - delegationMap.get(existingSubschema).push(fieldNode); - } else { - delegationMap.set(nonUniqueSubschemas[0], [fieldNode]); - } - }); - - const finalDelegationMap: Map = new Map(); - - delegationMap.forEach((selections, subschema) => { - finalDelegationMap.set(subschema, { - kind: Kind.SELECTION_SET, - selections, - }); - }); - - return { - delegationMap: finalDelegationMap, - unproxiableFieldNodes, - }; -}); - -const combineSubschemas = memoize2(function ( - subschemaOrSubschemas: Subschema | Array, - additionalSubschemas: Array -): Array { - return Array.isArray(subschemaOrSubschemas) - ? subschemaOrSubschemas.concat(additionalSubschemas) - : [subschemaOrSubschemas].concat(additionalSubschemas); -}); - -export function mergeFields( - mergedTypeInfo: MergedTypeInfo, - typeName: string, - object: any, - fieldNodes: Array, - sourceSubschemaOrSourceSubschemas: Subschema | Array, - targetSubschemas: Array, - context: Record, - info: GraphQLResolveInfo -): any { - if (!fieldNodes.length) { - return object; - } - - const { proxiableSubschemas, nonProxiableSubschemas } = sortSubschemasByProxiability( - mergedTypeInfo, - sourceSubschemaOrSourceSubschemas, - targetSubschemas, - fieldNodes - ); - - const { delegationMap, unproxiableFieldNodes } = buildDelegationPlan(mergedTypeInfo, fieldNodes, proxiableSubschemas); - - if (!delegationMap.size) { - return object; - } - - const resultMap: Map, SelectionSetNode> = new Map(); - delegationMap.forEach((selectionSet: SelectionSetNode, s: Subschema) => { - const resolver = mergedTypeInfo.resolvers.get(s); - const valueOrPromise = new ValueOrPromise(() => resolver(object, context, info, s, selectionSet)).catch(error => error); - resultMap.set(valueOrPromise, selectionSet); - }); - - return ValueOrPromise.all(Array.from(resultMap.keys())).then(results => - mergeFields( - mergedTypeInfo, - typeName, - mergeExternalObjects( - info.schema, - responsePathAsArray(info.path), - object.__typename, - object, - results, - Array.from(resultMap.values()) - ), - unproxiableFieldNodes, - combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), - nonProxiableSubschemas, - context, - info - ) - ).resolve(); -} - -const subschemaTypesContainSelectionSet = memoize3(function ( - mergedTypeInfo: MergedTypeInfo, - sourceSubschemaOrSourceSubschemas: Subschema | Array, - selectionSet: SelectionSetNode -) { - if (Array.isArray(sourceSubschemaOrSourceSubschemas)) { - return typesContainSelectionSet( - sourceSubschemaOrSourceSubschemas.map( - sourceSubschema => sourceSubschema.transformedSchema.getType(mergedTypeInfo.typeName) as GraphQLObjectType - ), - selectionSet - ); - } - - return typesContainSelectionSet( - [sourceSubschemaOrSourceSubschemas.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/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 73495d174ce..71230fd1817 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -6,8 +6,6 @@ import { isListType, GraphQLError, GraphQLSchema, - GraphQLCompositeType, - isAbstractType, GraphQLList, GraphQLType, locatedError, @@ -15,11 +13,9 @@ import { import AggregateError from '@ardatan/aggregate-error'; -import { StitchingInfo, SubschemaConfig } from './types'; +import { SubschemaConfig } from './types'; import { annotateExternalObject, isExternalObject } from './externalObjects'; -import { getFieldsNotInSubschema } from './getFieldsNotInSubschema'; -import { mergeFields } from './mergeFields'; -import { Subschema } from './Subschema'; +import { Receiver } from './Receiver'; export function resolveExternalValue( result: any, @@ -27,8 +23,8 @@ export function resolveExternalValue( subschema: GraphQLSchema | SubschemaConfig, context: Record, info: GraphQLResolveInfo, - returnType = info.returnType, - skipTypeMerging?: boolean + receiver?: Receiver, + returnType = info?.returnType ): any { const type = getNullableType(returnType); @@ -43,20 +39,18 @@ export function resolveExternalValue( if (isLeafType(type)) { return type.parseValue(result); } else if (isCompositeType(type)) { - return resolveExternalObject(type, result, unpathedErrors, subschema, context, info, skipTypeMerging); + return resolveExternalObject(result, unpathedErrors, subschema, info, receiver); } else if (isListType(type)) { - return resolveExternalList(type, result, unpathedErrors, subschema, context, info, skipTypeMerging); + return resolveExternalList(type, result, unpathedErrors, subschema, context, info, receiver); } } function resolveExternalObject( - type: GraphQLCompositeType, object: any, unpathedErrors: Array, subschema: GraphQLSchema | SubschemaConfig, - context: Record, info: GraphQLResolveInfo, - skipTypeMerging?: boolean + receiver?: Receiver ) { // 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 @@ -64,53 +58,9 @@ function resolveExternalObject( return object; } - annotateExternalObject(object, unpathedErrors, subschema); - - const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; - if (skipTypeMerging || !stitchingInfo) { - return object; - } - - let typeName: string; - - if (isAbstractType(type)) { - const resolvedType = info.schema.getTypeMap()[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; - } + annotateExternalObject(object, unpathedErrors, subschema, info, receiver); - const mergedTypeInfo = stitchingInfo.mergedTypes[typeName]; - let targetSubschemas: 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) { - return object; - } - - const fieldNodes = getFieldsNotInSubschema(info, subschema, mergedTypeInfo); - - return mergeFields( - mergedTypeInfo, - typeName, - object, - fieldNodes, - subschema as Subschema, - targetSubschemas, - context, - info - ); + return object; } function resolveExternalList( @@ -120,7 +70,7 @@ function resolveExternalList( subschema: GraphQLSchema | SubschemaConfig, context: Record, info: GraphQLResolveInfo, - skipTypeMerging?: boolean + receiver?: Receiver ) { return list.map(listMember => resolveExternalListMember( @@ -130,7 +80,7 @@ function resolveExternalList( subschema, context, info, - skipTypeMerging + receiver ) ); } @@ -142,7 +92,7 @@ function resolveExternalListMember( subschema: GraphQLSchema | SubschemaConfig, context: Record, info: GraphQLResolveInfo, - skipTypeMerging?: boolean + receiver?: Receiver ): any { if (listMember instanceof Error) { return listMember; @@ -155,9 +105,9 @@ function resolveExternalListMember( if (isLeafType(type)) { return type.parseValue(listMember); } else if (isCompositeType(type)) { - return resolveExternalObject(type, listMember, unpathedErrors, subschema, context, info, skipTypeMerging); + return resolveExternalObject(listMember, unpathedErrors, subschema, info, receiver); } else if (isListType(type)) { - return resolveExternalList(type, listMember, unpathedErrors, subschema, context, info, skipTypeMerging); + return resolveExternalList(type, listMember, unpathedErrors, subschema, context, info, receiver); } } diff --git a/packages/delegate/src/symbols.ts b/packages/delegate/src/symbols.ts index dad8b7b958a..16ad52d3729 100644 --- a/packages/delegate/src/symbols.ts +++ b/packages/delegate/src/symbols.ts @@ -1,3 +1,6 @@ export const UNPATHED_ERRORS_SYMBOL = Symbol('subschemaErrors'); export const OBJECT_SUBSCHEMA_SYMBOL = Symbol('initialSubschema'); +export const INITIAL_POSSIBLE_FIELDS = Symbol('initialPossibleFields'); +export const INFO_SYMBOL = Symbol('info'); export const FIELD_SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap'); +export const RECEIVER_MAP_SYMBOL = Symbol('receiverMap'); diff --git a/packages/delegate/src/transforms/AddFieldNodes.ts b/packages/delegate/src/transforms/AddFieldNodes.ts new file mode 100644 index 00000000000..24e34e5d421 --- /dev/null +++ b/packages/delegate/src/transforms/AddFieldNodes.ts @@ -0,0 +1,85 @@ +import { SelectionSetNode, TypeInfo, Kind, FieldNode, SelectionNode, print } from 'graphql'; + +import { Request } from '@graphql-tools/utils'; + +import { Transform, DelegationContext } from '../types'; +import { memoize2 } from '../memoize'; + +import VisitSelectionSets from './VisitSelectionSets'; + +export default class AddFieldNodes implements Transform { + private readonly transformer: VisitSelectionSets; + + constructor( + fieldNodesByField: Record>>, + dynamicFieldNodesByField: Record Array>>> + ) { + this.transformer = new VisitSelectionSets((node, typeInfo) => + visitSelectionSet(node, typeInfo, fieldNodesByField, dynamicFieldNodesByField) + ); + } + + public transformRequest( + originalRequest: Request, + delegationContext: DelegationContext, + transformationContext: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } +} + +function visitSelectionSet( + node: SelectionSetNode, + typeInfo: TypeInfo, + fieldNodesByField: Record>>, + dynamicFieldNodesByField: Record Array>>> +): SelectionSetNode { + const parentType = typeInfo.getParentType(); + + const newSelections: Map = new Map(); + + if (parentType != null) { + const parentTypeName = parentType.name; + addSelectionsToMap(newSelections, node.selections); + + if (parentTypeName in fieldNodesByField) { + node.selections.forEach(selection => { + if (selection.kind === Kind.FIELD) { + const name = selection.name.value; + const fieldNodes = fieldNodesByField[parentTypeName][name]; + if (fieldNodes != null) { + addSelectionsToMap(newSelections, fieldNodes); + } + } + }); + } + + if (parentTypeName in dynamicFieldNodesByField) { + node.selections.forEach(selection => { + if (selection.kind === Kind.FIELD) { + const name = selection.name.value; + const dynamicFieldNodes = dynamicFieldNodesByField[parentTypeName][name]; + if (dynamicFieldNodes != null) { + dynamicFieldNodes.forEach(fieldNodeFn => { + const fieldNodes = fieldNodeFn(selection); + if (fieldNodes != null) { + addSelectionsToMap(newSelections, fieldNodes); + } + }); + } + } + }); + } + + return { + ...node, + selections: Array.from(newSelections.values()), + }; + } +} + +const addSelectionsToMap = memoize2(function (map: Map, selections: ReadonlyArray): void { + selections.forEach(selection => { + map.set(print(selection), selection); + }); +}); diff --git a/packages/delegate/src/transforms/AddSelectionSets.ts b/packages/delegate/src/transforms/AddSelectionSets.ts deleted file mode 100644 index d725fd1c364..00000000000 --- a/packages/delegate/src/transforms/AddSelectionSets.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { SelectionSetNode, TypeInfo, Kind, FieldNode, SelectionNode, print } from 'graphql'; - -import { Request } from '@graphql-tools/utils'; - -import { Transform, DelegationContext } from '../types'; -import { memoize2 } from '../memoize'; - -import VisitSelectionSets from './VisitSelectionSets'; - -export default class AddSelectionSets implements Transform { - private readonly transformer: VisitSelectionSets; - - constructor( - selectionSetsByType: Record, - selectionSetsByField: Record>, - dynamicSelectionSetsByField: Record SelectionSetNode>>> - ) { - this.transformer = new VisitSelectionSets((node, typeInfo) => - visitSelectionSet(node, typeInfo, selectionSetsByType, selectionSetsByField, dynamicSelectionSetsByField) - ); - } - - public transformRequest( - originalRequest: Request, - delegationContext: DelegationContext, - transformationContext: Record - ): Request { - return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); - } -} - -function visitSelectionSet( - node: SelectionSetNode, - typeInfo: TypeInfo, - selectionSetsByType: Record, - selectionSetsByField: Record>, - dynamicSelectionSetsByField: Record SelectionSetNode>>> -): SelectionSetNode { - const parentType = typeInfo.getParentType(); - - const newSelections: Map = new Map(); - - if (parentType != null) { - const parentTypeName = parentType.name; - addSelectionsToMap(newSelections, node); - - if (parentTypeName in selectionSetsByType) { - const selectionSet = selectionSetsByType[parentTypeName]; - addSelectionsToMap(newSelections, selectionSet); - } - - if (parentTypeName in selectionSetsByField) { - node.selections.forEach(selection => { - if (selection.kind === Kind.FIELD) { - const name = selection.name.value; - const selectionSet = selectionSetsByField[parentTypeName][name]; - if (selectionSet != null) { - addSelectionsToMap(newSelections, selectionSet); - } - } - }); - } - - if (parentTypeName in dynamicSelectionSetsByField) { - node.selections.forEach(selection => { - if (selection.kind === Kind.FIELD) { - const name = selection.name.value; - const dynamicSelectionSets = dynamicSelectionSetsByField[parentTypeName][name]; - if (dynamicSelectionSets != null) { - dynamicSelectionSets.forEach(selectionSetFn => { - const selectionSet = selectionSetFn(selection); - if (selectionSet != null) { - addSelectionsToMap(newSelections, selectionSet); - } - }); - } - } - }); - } - - return { - ...node, - selections: Array.from(newSelections.values()), - }; - } -} - -const addSelectionsToMap = memoize2(function (map: Map, selectionSet: SelectionSetNode): void { - selectionSet.selections.forEach(selection => { - map.set(print(selection), selection); - }); -}); diff --git a/packages/delegate/src/transforms/AddTypenameToAbstract.ts b/packages/delegate/src/transforms/AddTypename.ts similarity index 76% rename from packages/delegate/src/transforms/AddTypenameToAbstract.ts rename to packages/delegate/src/transforms/AddTypename.ts index 4ffb31f57a7..0a14cac6828 100644 --- a/packages/delegate/src/transforms/AddTypenameToAbstract.ts +++ b/packages/delegate/src/transforms/AddTypename.ts @@ -7,20 +7,19 @@ import { SelectionSetNode, Kind, GraphQLSchema, - isAbstractType, } from 'graphql'; import { Request } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; -export default class AddTypenameToAbstract implements Transform { +export default class AddTypename implements Transform { public transformRequest( originalRequest: Request, delegationContext: DelegationContext, _transformationContext: Record ): Request { - const document = addTypenameToAbstract(delegationContext.targetSchema, originalRequest.document); + const document = addTypename(delegationContext.targetSchema, originalRequest.document); return { ...originalRequest, document, @@ -28,15 +27,16 @@ export default class AddTypenameToAbstract implements Transform { } } -function addTypenameToAbstract(targetSchema: GraphQLSchema, document: DocumentNode): DocumentNode { +function addTypename(targetSchema: GraphQLSchema, document: DocumentNode): DocumentNode { const typeInfo = new TypeInfo(targetSchema); + const subscriptionType = targetSchema.getSubscriptionType(); return visit( document, visitWithTypeInfo(typeInfo, { [Kind.SELECTION_SET](node: SelectionSetNode): SelectionSetNode | null | undefined { const parentType: GraphQLType = typeInfo.getParentType(); let selections = node.selections; - if (parentType != null && isAbstractType(parentType)) { + if (parentType !== subscriptionType) { selections = selections.concat({ kind: Kind.FIELD, name: { diff --git a/packages/delegate/src/transforms/ExpandAbstractTypes.ts b/packages/delegate/src/transforms/ExpandAbstractTypes.ts index a2ba8d2abfa..a80a9f88f80 100644 --- a/packages/delegate/src/transforms/ExpandAbstractTypes.ts +++ b/packages/delegate/src/transforms/ExpandAbstractTypes.ts @@ -30,11 +30,9 @@ export default class ExpandAbstractTypes implements Transform { delegationContext.info.schema, targetSchema ); - const reversePossibleTypesMap = flipMapping(possibleTypesMap); const document = expandAbstractTypes( targetSchema, possibleTypesMap, - reversePossibleTypesMap, interfaceExtensionsMap, originalRequest.document ); @@ -79,24 +77,9 @@ function extractPossibleTypes(sourceSchema: GraphQLSchema, targetSchema: GraphQL return { possibleTypesMap, interfaceExtensionsMap }; } -function flipMapping(mapping: Record>): Record> { - const result: Record> = Object.create(null); - Object.keys(mapping).forEach(typeName => { - const toTypeNames = mapping[typeName]; - toTypeNames.forEach(toTypeName => { - if (!(toTypeName in result)) { - result[toTypeName] = []; - } - result[toTypeName].push(typeName); - }); - }); - return result; -} - function expandAbstractTypes( targetSchema: GraphQLSchema, possibleTypesMap: Record>, - reversePossibleTypesMap: Record>, interfaceExtensionsMap: Record>, document: DocumentNode ): DocumentNode { @@ -178,7 +161,7 @@ function expandAbstractTypes( visitWithTypeInfo(typeInfo, { [Kind.SELECTION_SET](node: SelectionSetNode) { let newSelections = node.selections; - const addedSelections = []; + const addedSelections: Array = []; const maybeType = typeInfo.getParentType(); if (maybeType != null) { const parentType: GraphQLNamedType = getNamedType(maybeType); @@ -226,16 +209,6 @@ function expandAbstractTypes( } }); - if (parentType.name in reversePossibleTypesMap) { - addedSelections.push({ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: '__typename', - }, - }); - } - if (interfaceExtensionFields.length) { const possibleTypes = possibleTypesMap[parentType.name]; if (possibleTypes != null) { diff --git a/packages/delegate/src/transforms/StoreAsyncSelectionSets.ts b/packages/delegate/src/transforms/StoreAsyncSelectionSets.ts new file mode 100644 index 00000000000..b03266e0d87 --- /dev/null +++ b/packages/delegate/src/transforms/StoreAsyncSelectionSets.ts @@ -0,0 +1,205 @@ +import { + DirectiveNode, + DocumentNode, + FieldNode, + FragmentSpreadNode, + InlineFragmentNode, + Kind, + SelectionSetNode, + visit, +} from 'graphql'; + +import { Request } from '@graphql-tools/utils'; + +import { Transform, DelegationContext } from '../types'; + +export default class StoreAsyncSelectionSets implements Transform { + private labelNumber: number; + + constructor() { + this.labelNumber = 0; + } + + public transformRequest( + originalRequest: Request, + delegationContext: DelegationContext, + _transformationContext: Record + ): Request { + const { asyncSelectionSets } = delegationContext; + return { + ...originalRequest, + document: this.storeAsyncSelectionSets(originalRequest.document, asyncSelectionSets), + }; + } + + private storeAsyncSelectionSets( + document: DocumentNode, + asyncSelectionSets: Record + ): DocumentNode { + const fragmentSelectionSets: Record = Object.create(null); + + document.definitions.forEach(def => { + if (def.kind === Kind.FRAGMENT_DEFINITION) { + fragmentSelectionSets[def.name.value] = filterSelectionSet(def.selectionSet); + } + }); + + return visit(document, { + [Kind.FIELD]: node => { + const newNode = transformFieldNode(node, this.labelNumber); + + if (newNode === undefined) { + return; + } + + if (node.selectionSet !== undefined) { + asyncSelectionSets[`label_${this.labelNumber}`] = filterSelectionSet(node.selectionSet); + } + + this.labelNumber++; + + return newNode; + }, + [Kind.INLINE_FRAGMENT]: node => { + const newNode = transformFragmentNode(node, this.labelNumber); + + if (newNode === undefined) { + return; + } + + asyncSelectionSets[`label_${this.labelNumber}`] = filterSelectionSet(node.selectionSet); + + this.labelNumber++; + + return newNode; + }, + [Kind.FRAGMENT_SPREAD]: node => { + const newNode = transformFragmentNode(node, this.labelNumber); + + if (newNode === undefined) { + return; + } + + asyncSelectionSets[this.labelNumber] = fragmentSelectionSets[node.name.value]; + + this.labelNumber++; + + return newNode; + }, + }); + } +} + +function transformFragmentNode(node: T, labelNumber: number): T { + const deferIndex = node.directives?.findIndex(directive => directive.name.value === 'defer'); + if (deferIndex === undefined || deferIndex === -1) { + return; + } + + const defer = node.directives[deferIndex]; + + let newDefer: DirectiveNode; + + const args = defer.arguments; + const labelIndex = args?.findIndex(arg => arg.name.value === 'label'); + const newLabel = { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'label', + }, + value: { + kind: Kind.STRING, + value: `label_${labelNumber}`, + }, + }; + + if (labelIndex === undefined) { + newDefer = { + ...defer, + arguments: [newLabel], + }; + } else if (labelIndex === -1) { + newDefer = { + ...defer, + arguments: [...args, newLabel], + }; + } else { + const newArgs = args.slice(); + newArgs.splice(labelIndex, 1, newLabel); + newDefer = { + ...defer, + arguments: newArgs, + }; + } + + const newDirectives = node.directives.slice(); + newDirectives.splice(deferIndex, 1, newDefer); + + return { + ...node, + directives: newDirectives, + }; +} + +function transformFieldNode(node: FieldNode, labelNumber: number): FieldNode { + const streamIndex = node.directives?.findIndex(directive => directive.name.value === 'stream'); + if (streamIndex === undefined || streamIndex === -1) { + return; + } + + const stream = node.directives[streamIndex]; + + let newStream: DirectiveNode; + + const args = stream.arguments; + const labelIndex = args?.findIndex(arg => arg.name.value === 'label'); + const newLabel = { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'label', + }, + value: { + kind: Kind.STRING, + value: `label_${labelNumber}`, + }, + }; + + if (labelIndex === undefined) { + newStream = { + ...stream, + arguments: [newLabel], + }; + } else if (labelIndex === -1) { + newStream = { + ...stream, + arguments: [...args, newLabel], + }; + } else { + const newArgs = args.slice(); + newArgs.splice(labelIndex, 1, newLabel); + newStream = { + ...stream, + arguments: newArgs, + }; + } + + const newDirectives = node.directives.slice(); + newDirectives.splice(streamIndex, 1, newStream); + + return { + ...node, + directives: newDirectives, + }; +} + +function filterSelectionSet(selectionSet: SelectionSetNode): SelectionSetNode { + return { + ...selectionSet, + selections: selectionSet.selections.filter( + selection => + selection.directives === undefined || !selection.directives.some(directive => directive.name.value === 'defer') + ), + }; +} diff --git a/packages/delegate/src/transforms/VisitSelectionSets.ts b/packages/delegate/src/transforms/VisitSelectionSets.ts index 3f807241b0c..690fad35466 100644 --- a/packages/delegate/src/transforms/VisitSelectionSets.ts +++ b/packages/delegate/src/transforms/VisitSelectionSets.ts @@ -7,13 +7,13 @@ import { visit, visitWithTypeInfo, GraphQLOutputType, - OperationDefinitionNode, FragmentDefinitionNode, SelectionNode, DefinitionNode, + InlineFragmentNode, } from 'graphql'; -import { Request, collectFields, GraphQLExecutionContext } from '@graphql-tools/utils'; +import { Request } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; @@ -48,89 +48,55 @@ function visitSelectionSets( initialType: GraphQLOutputType, visitor: (node: SelectionSetNode, typeInfo: TypeInfo) => SelectionSetNode ): DocumentNode { - const { document, variables } = request; + const { document } = request; - const operations: Array = []; - const fragments: Record = Object.create(null); + const typeInfo = new TypeInfo(schema, undefined, initialType); + + const newDefinitions: Array = []; document.definitions.forEach(def => { - if (def.kind === Kind.OPERATION_DEFINITION) { - operations.push(def); - } else if (def.kind === Kind.FRAGMENT_DEFINITION) { - fragments[def.name.value] = def; - } - }); + if (def.kind === Kind.FRAGMENT_DEFINITION) { + newDefinitions.push(visitNode(def, typeInfo, visitor)); + } else if (def.kind === Kind.OPERATION_DEFINITION) { + const newSelections: Array = []; - const partialExecutionContext = { - schema, - variableValues: variables, - fragments, - } as GraphQLExecutionContext; + def.selectionSet.selections.forEach(selection => { + if (selection.kind === Kind.FRAGMENT_SPREAD) { + return; + } - const typeInfo = new TypeInfo(schema, undefined, initialType); - const newDefinitions: Array = operations.map(operation => { - const type = - operation.operation === 'query' - ? schema.getQueryType() - : operation.operation === 'mutation' - ? schema.getMutationType() - : schema.getSubscriptionType(); - - const fields = collectFields( - partialExecutionContext, - type, - operation.selectionSet, - Object.create(null), - Object.create(null) - ); + if (selection.kind === Kind.INLINE_FRAGMENT) { + newSelections.push(visitNode(selection, typeInfo, visitor)); + return; + } - const newSelections: Array = []; - Object.keys(fields).forEach(responseKey => { - const fieldNodes = fields[responseKey]; - fieldNodes.forEach(fieldNode => { - const selectionSet = fieldNode.selectionSet; + const selectionSet = selection.selectionSet; if (selectionSet == null) { - newSelections.push(fieldNode); + newSelections.push(selection); return; } - const newSelectionSet = visit( - selectionSet, - visitWithTypeInfo(typeInfo, { - [Kind.SELECTION_SET]: node => visitor(node, typeInfo), - }) - ); + const newSelectionSet = visitNode(selectionSet, typeInfo, visitor); if (newSelectionSet === selectionSet) { - newSelections.push(fieldNode); + newSelections.push(selection); return; } newSelections.push({ - ...fieldNode, + ...selection, selectionSet: newSelectionSet, }); }); - }); - return { - ...operation, - selectionSet: { - kind: Kind.SELECTION_SET, - selections: newSelections, - }, - }; - }); - - Object.values(fragments).forEach(fragment => { - newDefinitions.push( - visit( - fragment, - visitWithTypeInfo(typeInfo, { - [Kind.SELECTION_SET]: node => visitor(node, typeInfo), - }) - ) - ); + newDefinitions.push({ + ...def, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: newSelections, + }, + }); + } }); return { @@ -138,3 +104,16 @@ function visitSelectionSets( definitions: newDefinitions, }; } + +function visitNode( + node: T, + typeInfo: TypeInfo, + visitor: (node: SelectionSetNode, typeInfo: TypeInfo) => SelectionSetNode +): T { + return visit( + node, + visitWithTypeInfo(typeInfo, { + [Kind.SELECTION_SET]: node => visitor(node, typeInfo), + }) + ); +} diff --git a/packages/delegate/src/transforms/index.ts b/packages/delegate/src/transforms/index.ts index 624bc89e3fa..9b59ef1bb27 100644 --- a/packages/delegate/src/transforms/index.ts +++ b/packages/delegate/src/transforms/index.ts @@ -1,7 +1,7 @@ -export { default as CheckResultAndHandleErrors, checkResultAndHandleErrors } from './CheckResultAndHandleErrors'; export { default as ExpandAbstractTypes } from './ExpandAbstractTypes'; export { default as VisitSelectionSets } from './VisitSelectionSets'; -export { default as AddSelectionSets } from './AddSelectionSets'; +export { default as AddFieldNodes } from './AddFieldNodes'; export { default as AddArgumentsAsVariables } from './AddArgumentsAsVariables'; export { default as FilterToSchema } from './FilterToSchema'; -export { default as AddTypenameToAbstract } from './AddTypenameToAbstract'; +export { default as AddTypename } from './AddTypename'; +export { default as StoreAsyncSelectionSets } from './StoreAsyncSelectionSets'; diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 419f11bd075..ff323ef04d0 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -10,14 +10,24 @@ import { VariableDefinitionNode, OperationTypeNode, GraphQLError, + GraphQLFieldMap, } from 'graphql'; import DataLoader from 'dataloader'; import { ExecutionParams, ExecutionResult, Executor, Request, Subscriber, TypeMap } from '@graphql-tools/utils'; +import { + OBJECT_SUBSCHEMA_SYMBOL, + FIELD_SUBSCHEMA_MAP_SYMBOL, + UNPATHED_ERRORS_SYMBOL, + RECEIVER_MAP_SYMBOL, + INITIAL_POSSIBLE_FIELDS, + INFO_SYMBOL, +} from './symbols'; + import { Subschema } from './Subschema'; -import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; +import { Receiver } from './Receiver'; export type SchemaTransform = ( originalWrappingSchema: GraphQLSchema, @@ -55,7 +65,7 @@ export interface DelegationContext { onLocatedError?: (originalError: GraphQLError) => GraphQLError; transforms: Array; transformedSchema: GraphQLSchema; - skipTypeMerging: boolean; + asyncSelectionSets: Record; } export type DelegationBinding = (delegationContext: DelegationContext) => Array; @@ -76,7 +86,6 @@ export interface IDelegateToSchemaOptions, TArgs transforms?: Array; transformedSchema?: GraphQLSchema; skipValidation?: boolean; - skipTypeMerging?: boolean; binding?: DelegationBinding; } @@ -115,6 +124,7 @@ export interface MergedTypeInfo> { targetSubschemas: Map>; uniqueFields: Record; nonUniqueFields: Record>; + subschemaFields: Record; typeMaps: Map; selectionSets: Map; fieldSelectionSets: Map>; @@ -186,15 +196,18 @@ export type MergedTypeResolver> = ( export interface StitchingInfo> { subschemaMap: Map, Subschema>; - selectionSetsByType: Record; - selectionSetsByField: Record>; - dynamicSelectionSetsByField: Record SelectionSetNode>>>; + fieldNodesByField: Record>>; + dynamicFieldNodesByField: Record Array>>>; mergedTypes: Record>; } export interface ExternalObject> { - key: any; + __typename: string; + [key: string]: any; [OBJECT_SUBSCHEMA_SYMBOL]: GraphQLSchema | SubschemaConfig; + [INITIAL_POSSIBLE_FIELDS]: GraphQLFieldMap; + [INFO_SYMBOL]: GraphQLResolveInfo; [FIELD_SUBSCHEMA_MAP_SYMBOL]: Record>; [UNPATHED_ERRORS_SYMBOL]: Array; + [RECEIVER_MAP_SYMBOL]: Map; } diff --git a/packages/delegate/tests/defer.test.ts b/packages/delegate/tests/defer.test.ts new file mode 100644 index 00000000000..40b380e75f5 --- /dev/null +++ b/packages/delegate/tests/defer.test.ts @@ -0,0 +1,329 @@ +import { graphql } from 'graphql'; + +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stitchSchemas } from '@graphql-tools/stitch'; +import { ExecutionResult, isAsyncIterable } from '@graphql-tools/utils'; + +describe('defer support', () => { + test('should work for root fields', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + test: String + } + `, + resolvers: { + Query: { + test: () => 'test', + } + }, + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [schema] + }); + + const result = await graphql( + stitchedSchema, + ` + query { + ... on Query @defer { + test + } + } + `, + ); + + const results = []; + if (isAsyncIterable(result)) { + for await (const patch of result) { + results.push(patch); + } + } + + expect(results[0]).toEqual({ + data: {}, + hasNext: true, + }); + expect(results[1]).toEqual({ + data: { + test: 'test', + }, + hasNext: false, + path: [], + }); + }); + + test('should work for proxied fields', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Object { + test: String + } + type Query { + object: Object + } + `, + resolvers: { + Object: { + test: () => 'test', + }, + Query: { + object: () => ({}), + } + }, + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [schema] + }); + + const result = await graphql( + stitchedSchema, + ` + query { + object { + ... on Object @defer { + test + } + } + } + `, + ); + + const results = []; + if (isAsyncIterable(result)) { + for await (const patch of result) { + results.push(patch); + } + } + + expect(results[0]).toEqual({ + data: { object: {} }, + hasNext: true, + }); + expect(results[1]).toEqual({ + data: { + test: 'test', + }, + hasNext: false, + path: ['object'], + }); + }); + + test('should work for proxied fields from multiple schemas', async () => { + const schema1 = makeExecutableSchema({ + typeDefs: ` + type Object { + id: ID + field1: String + } + type Query { + object(id: ID): Object + } + `, + resolvers: { + Query: { + object: () => ({ id: '1', field1: 'field1' }), + } + }, + }); + + const schema2 = makeExecutableSchema({ + typeDefs: ` + type Object { + id: ID + field2: String + } + type Query { + object(id: ID): Object + } + `, + resolvers: { + Query: { + object: () => ({ id: '1', field2: 'field2' }), + } + }, + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [{ + schema: schema1, + merge: { + Object: { + selectionSet: '{ id }', + fieldName: 'object', + args: ({ id }) => ({ id }), + }, + }, + }, { + schema: schema2, + merge: { + Object: { + selectionSet: '{ id }', + fieldName: 'object', + args: ({ id }) => ({ id }), + }, + }, + }], + }); + + const result = await graphql( + stitchedSchema, + ` + query { + object(id: "1") { + ... on Object @defer { + field1 + field2 + } + } + } + `, + ); + + expect((result as ExecutionResult).errors).toBeUndefined(); + + const results = []; + + if (isAsyncIterable(result)) { + for await (const patch of result) { + results.push(patch); + } + } + + expect(results[0]).toEqual({ + data: { object: {} }, + hasNext: true, + }); + expect(results[1]).toEqual({ + data: { + field1: 'field1', + field2: 'field2', + }, + hasNext: false, + path: ['object'], + }); + }); + + test('should work for nested proxied fields from multiple schemas', async () => { + const schema1 = makeExecutableSchema({ + typeDefs: ` + type Object { + field: Subtype + } + type Subtype { + id: ID + subfield1: String + } + type Query { + object: Object + field(id: ID): Subtype + } + `, + resolvers: { + Query: { + object: () => ({ field: { id: '1', subfield1: 'subfield1'} }), + field: () => ({ id: '1', subfield1: 'subfield1'}), + }, + }, + }); + + const schema2 = makeExecutableSchema({ + typeDefs: ` + type Object { + field: Subtype + } + type Subtype { + id: ID + subfield2: String + } + type Query { + object: Object + field(id: ID): Subtype + } + `, + resolvers: { + Query: { + object: () => ({ field: { id: '1', subfield2: 'subfield2'} }), + field: () => ({ id: '1', subfield2: 'subfield2'}), + }, + }, + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [{ + schema: schema1, + merge: { + Subtype: { + selectionSet: '{ id }', + fieldName: 'field', + args: ({ id }) => ({ id }), + }, + }, + }, { + schema: schema2, + merge: { + Subtype: { + selectionSet: '{ id }', + fieldName: 'field', + args: ({ id }) => ({ id }), + }, + }, + }], + }); + + const result = await graphql( + stitchedSchema, + ` + query { + object { + ... on Object @defer { + field { + subfield1 + } + } + ... on Object @defer { + field { + subfield2 + } + } + } + } + `, + ); + + expect((result as ExecutionResult).errors).toBeUndefined(); + + const results = []; + + if (isAsyncIterable(result)) { + for await (const patch of result) { + results.push(patch); + } + } + + expect(results[0]).toEqual({ + data: { object: {} }, + hasNext: true, + }); + expect(results[1]).toEqual({ + data: { + field: { + subfield2: 'subfield2', + }, + }, + hasNext: true, + path: ['object'], + }); + expect(results[2]).toEqual({ + data: { + field: { + subfield1: 'subfield1', + }, + }, + hasNext: false, + path: ['object'], + }); + }); +}); diff --git a/packages/delegate/tests/errors.test.ts b/packages/delegate/tests/errors.test.ts index faf8d5b2ff0..55f2eb80882 100644 --- a/packages/delegate/tests/errors.test.ts +++ b/packages/delegate/tests/errors.test.ts @@ -1,10 +1,10 @@ -import { GraphQLError, GraphQLResolveInfo, locatedError, graphql } from 'graphql'; +import { GraphQLError, locatedError, graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ExecutionResult } from '@graphql-tools/utils'; import { stitchSchemas } from '@graphql-tools/stitch'; +import { DelegationContext, externalValueFromResult } from '@graphql-tools/delegate'; -import { checkResultAndHandleErrors } from '../src/transforms/CheckResultAndHandleErrors'; import { UNPATHED_ERRORS_SYMBOL } from '../src/symbols'; import { getUnpathedErrors } from '../src/externalObjects'; import { delegateToSchema, defaultMergedResolver } from '../src'; @@ -32,17 +32,15 @@ describe('Errors', () => { }); }); - describe('checkResultAndHandleErrors', () => { + describe('CheckResultAndHandleErrors', () => { test('persists single error', () => { const result = { errors: [new GraphQLError('Test error')], }; try { - checkResultAndHandleErrors( + externalValueFromResult( result, - {}, - ({} as unknown) as GraphQLResolveInfo, - 'responseKey', + { fieldName: 'responseKey' } as unknown as DelegationContext, ); } catch (e) { expect(e.message).toEqual('Test error'); @@ -55,11 +53,9 @@ describe('Errors', () => { errors: [new ErrorWithExtensions('Test error', 'UNAUTHENTICATED')], }; try { - checkResultAndHandleErrors( + externalValueFromResult( result, - {}, - ({} as unknown) as GraphQLResolveInfo, - 'responseKey', + { fieldName: 'responseKey '} as unknown as DelegationContext, ); } catch (e) { expect(e.message).toEqual('Test error'); @@ -73,11 +69,9 @@ describe('Errors', () => { errors: [new GraphQLError('Error1'), new GraphQLError('Error2')], }; try { - checkResultAndHandleErrors( + externalValueFromResult( result, - {}, - ({} as unknown) as GraphQLResolveInfo, - 'responseKey', + { fieldName: 'reponseKey' } as unknown as DelegationContext, ); } catch (e) { expect(e.message).toEqual('Error1\nError2'); diff --git a/packages/delegate/tests/stream.test.ts b/packages/delegate/tests/stream.test.ts new file mode 100644 index 00000000000..83f1967c16b --- /dev/null +++ b/packages/delegate/tests/stream.test.ts @@ -0,0 +1,107 @@ +import { graphql } from 'graphql'; + +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stitchSchemas } from '@graphql-tools/stitch'; +import { isAsyncIterable } from '@graphql-tools/utils'; + +describe('stream support', () => { + test('should work for root fields', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + test: [String] + } + `, + resolvers: { + Query: { + test: () => ['test1', 'test2'], + } + }, + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [schema] + }); + + const result = await graphql( + stitchedSchema, + ` + query { + test @stream(initialCount: 1) + } + `, + ); + + const results = []; + if (isAsyncIterable(result)) { + for await (const patch of result) { + results.push(patch); + } + } + + expect(results[0]).toEqual({ + data: { + test: ['test1'], + }, + hasNext: true, + }); + expect(results[1]).toEqual({ + data: 'test2', + hasNext: false, + path: ['test', 1], + }); + }); + + test('should work for proxied fields', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Object { + test: [String] + } + type Query { + object: Object + } + `, + resolvers: { + Object: { + test: () => ['test1', 'test2'], + }, + Query: { + object: () => ({}), + } + }, + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [schema] + }); + + const result = await graphql( + stitchedSchema, + ` + query { + object { + test @stream(initialCount: 1) + } + } + `, + ); + + const results = []; + if (isAsyncIterable(result)) { + for await (const patch of result) { + results.push(patch); + } + } + + expect(results[0]).toEqual({ + data: { object: { test: ['test1'] } }, + hasNext: true, + }); + expect(results[1]).toEqual({ + data: 'test2', + hasNext: true, + path: ['object', 'test', 1], + }); + }); +}); diff --git a/packages/stitch/src/createMergedTypeResolver.ts b/packages/stitch/src/createMergedTypeResolver.ts index 21bb0dbc1ed..6d010ab6c57 100644 --- a/packages/stitch/src/createMergedTypeResolver.ts +++ b/packages/stitch/src/createMergedTypeResolver.ts @@ -20,7 +20,6 @@ export function createMergedTypeResolver(mergedTypeResolverOptions: MergedTypeRe selectionSet, context, info, - skipTypeMerging: true, }); } @@ -37,7 +36,6 @@ export function createMergedTypeResolver(mergedTypeResolverOptions: MergedTypeRe selectionSet, context, info, - skipTypeMerging: true, }); } diff --git a/packages/stitch/tests/alternateStitchSchemas.test.ts b/packages/stitch/tests/alternateStitchSchemas.test.ts index 3ae5cb889f1..9514ca50157 100644 --- a/packages/stitch/tests/alternateStitchSchemas.test.ts +++ b/packages/stitch/tests/alternateStitchSchemas.test.ts @@ -2004,7 +2004,6 @@ describe('basic type merging', () => { selectionSet, context, info, - skipTypeMerging: true, }), }, }, @@ -2024,7 +2023,6 @@ describe('basic type merging', () => { selectionSet, context, info, - skipTypeMerging: true, }), }, }, @@ -2148,7 +2146,6 @@ describe('unidirectional type merging', () => { selectionSet, context, info, - skipTypeMerging: true, }), }, }, diff --git a/yarn.lock b/yarn.lock index 76a579c58fd..3b64b80ec25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2143,6 +2143,11 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== +"@repeaterjs/repeater@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.4.tgz#a04d63f4d1bf5540a41b01a921c9a7fddc3bd1ca" + integrity sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA== + "@rollup/plugin-node-resolve@7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.1.tgz#8c6e59c4b28baf9d223028d0e450e06a485bb2b7" @@ -6622,10 +6627,10 @@ graphql-ws@^4.4.1: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.4.1.tgz#69f472f362b57366af23265c6c6b967077b9d1dc" integrity sha512-kHgDohfRQFDdzXzLqsV4wZM141sO1ukaXW/RSLlmIUsxT4N3r/4eQYTbkeLd4yRXaDkmv/rYf1EHL09Y5KO+Uw== -graphql@15.5.0, graphql@^14.5.3, graphql@^15.3.0: - version "15.5.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" - integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== +graphql@15.4.0-experimental-stream-defer.1, graphql@^14.5.3, graphql@^15.3.0: + version "15.4.0-experimental-stream-defer.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.4.0-experimental-stream-defer.1.tgz#46ae3fd2b532284575e7ddcf6c4f08aa7fe53fa3" + integrity sha512-zlGgY7aLlIofjO0CfTpCYK/tMccnj+5jvjnkTnW5qOxYhgEltuCvpMNYOJ67gz6L1flTIigt5BVEM8JExgtW3w== gray-matter@^4.0.3: version "4.0.3" From bb8b61239188bd5dc4954380a98487be63a3ef63 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:03:21 +0300 Subject: [PATCH 04/49] remove unnecessary lint message --- packages/utils/src/collectFields.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/utils/src/collectFields.ts b/packages/utils/src/collectFields.ts index aef6ab5d9b9..22e201e8454 100644 --- a/packages/utils/src/collectFields.ts +++ b/packages/utils/src/collectFields.ts @@ -122,6 +122,5 @@ function doesFragmentConditionMatch( * Implements the logic to compute the key of a given field's entry */ function getFieldEntryKey(node: FieldNode): string { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return node.alias ? node.alias.value : node.name.value; } From ae1e1f6a3d8aa328289e4ae5e237134c3b828cb4 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:05:16 +0300 Subject: [PATCH 05/49] update types --- packages/utils/src/Interfaces.ts | 12 ++++++++++++ packages/utils/src/executor.ts | 9 ++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index e8f0aade45b..144e3a3b209 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -22,6 +22,7 @@ import { OperationDefinitionNode, GraphQLError, ExecutionResult as GraphQLExecutionResult, + ExecutionPatchResult as GraphQLExecutionPatchResult, GraphQLOutputType, FieldDefinitionNode, GraphQLFieldConfig, @@ -54,10 +55,21 @@ import { SchemaVisitor } from './SchemaVisitor'; // See: https://github.com/graphql/graphql-js/pull/2490 export interface ExecutionResult> extends GraphQLExecutionResult { + errors?: ReadonlyArray; data?: TData | null; extensions?: Record; } +export interface ExecutionPatchResult> extends GraphQLExecutionPatchResult { + errors?: ReadonlyArray; + data?: TData | null; + path?: ReadonlyArray; + label?: string; + hasNext: boolean; + extensions?: Record; +} + +export type AsyncExecutionResult> = ExecutionResult | ExecutionPatchResult; // graphql-js non-exported typings export type TypeMap = Record; diff --git a/packages/utils/src/executor.ts b/packages/utils/src/executor.ts index 7ff1d6422a8..03bfced5f4f 100644 --- a/packages/utils/src/executor.ts +++ b/packages/utils/src/executor.ts @@ -1,5 +1,5 @@ import { DocumentNode, GraphQLResolveInfo } from 'graphql'; -import { ExecutionResult } from './Interfaces'; +import { AsyncExecutionResult, ExecutionResult } from './Interfaces'; export interface ExecutionParams, TContext = any> { document: DocumentNode; @@ -15,7 +15,8 @@ export type AsyncExecutor> = < TContext extends TBaseContext = TBaseContext >( params: ExecutionParams -) => Promise>; +) => Promise> | ExecutionResult>; + export type SyncExecutor> = < TReturn = Record, TArgs = Record, @@ -23,13 +24,15 @@ export type SyncExecutor> = < >( params: ExecutionParams ) => ExecutionResult; + export type Executor> = < TReturn = Record, TArgs = Record, TContext extends TBaseContext = TBaseContext >( params: ExecutionParams -) => ExecutionResult | Promise>; +) => ExecutionResult | AsyncIterableIterator> | Promise> | ExecutionResult>; + export type Subscriber> = < TReturn = Record, TArgs = Record, From 1b0f8fa7d383e724dfef3c5e77e7f1665571b93c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:07:48 +0300 Subject: [PATCH 06/49] Update mock package Just a types update --- packages/mock/src/types.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/mock/src/types.ts b/packages/mock/src/types.ts index e9307bdaa55..cb5d76c12de 100644 --- a/packages/mock/src/types.ts +++ b/packages/mock/src/types.ts @@ -1,4 +1,6 @@ -import { ExecutionResult, GraphQLSchema } from 'graphql'; +import { GraphQLSchema } from 'graphql'; + +import { ExecutionResult, AsyncExecutionResult } from '@graphql-tools/utils'; export type IMockFn = () => unknown; export type IScalarMock = unknown | IMockFn; @@ -212,5 +214,8 @@ export interface IMockServer { * @param query GraphQL query to execute * @param vars Variables */ - query: (query: string, vars?: Record) => Promise; + query: ( + query: string, + vars?: Record + ) => Promise>; } From 97ae0e7009a7c16d65e8193bc55f5d1970f63a80 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:10:35 +0300 Subject: [PATCH 07/49] Update stitch package Precompile selection set hints into arrays of field nodes Dynamic selectionSet hints therefore now return an array of field nodes instead of a selectionSet! --- packages/stitch/src/selectionSetArgs.ts | 65 ++++--- packages/stitch/src/stitchSchemas.ts | 57 +++++- packages/stitch/src/stitchingInfo.ts | 183 ++++++++++-------- packages/stitch/src/types.ts | 4 +- .../tests/alternateStitchSchemas.test.ts | 30 ++- packages/stitch/tests/fixtures/schemas.ts | 19 +- .../stitch/tests/selectionSetArgs.test.ts | 49 +++-- website/docs/stitch-api.md | 2 +- 8 files changed, 267 insertions(+), 142 deletions(-) diff --git a/packages/stitch/src/selectionSetArgs.ts b/packages/stitch/src/selectionSetArgs.ts index 4dbc164d5b2..bc8b1f4326b 100644 --- a/packages/stitch/src/selectionSetArgs.ts +++ b/packages/stitch/src/selectionSetArgs.ts @@ -1,29 +1,50 @@ -import { parseSelectionSet } from '@graphql-tools/utils'; -import { SelectionSetNode, SelectionNode, FieldNode, Kind } from 'graphql'; +import { collectFields, GraphQLExecutionContext, parseSelectionSet } from '@graphql-tools/utils'; +import { FieldNode, GraphQLSchema, GraphQLObjectType, GraphQLField, getNamedType } from 'graphql'; -export const forwardArgsToSelectionSet: ( +export function forwardArgsToSelectionSet( selectionSet: string, - mapping?: Record -) => (field: FieldNode) => SelectionSetNode = (selectionSet: string, mapping?: Record) => { + mapping?: Record> +): (schema: GraphQLSchema, field: GraphQLField) => (originalFieldNode: FieldNode) => Array { const selectionSetDef = parseSelectionSet(selectionSet, { noLocation: true }); - return (field: FieldNode): SelectionSetNode => { - const selections = selectionSetDef.selections.map( - (selectionNode): SelectionNode => { - if (selectionNode.kind === Kind.FIELD) { + + return (schema: GraphQLSchema, field: GraphQLField) => { + const partialExecutionContext = ({ + schema, + variableValues: Object.create(null), + fragments: Object.create(null), + } as unknown) as GraphQLExecutionContext; + + const responseKeys = collectFields( + partialExecutionContext, + getNamedType(field.type) as GraphQLObjectType, + selectionSetDef, + Object.create(null), + Object.create(null) + ); + + return (originalFieldNode: FieldNode): Array => { + const newFieldNodes: Array = []; + + Object.values(responseKeys).forEach(fieldNodes => { + fieldNodes.forEach(fieldNode => { if (!mapping) { - return { ...selectionNode, arguments: field.arguments.slice() }; - } else if (selectionNode.name.value in mapping) { - const selectionArgs = mapping[selectionNode.name.value]; - return { - ...selectionNode, - arguments: field.arguments.filter((arg): boolean => selectionArgs.includes(arg.name.value)), - }; + newFieldNodes.push({ + ...fieldNode, + arguments: originalFieldNode.arguments.slice(), + }); + } else if (fieldNode.name.value in mapping) { + const newArgs = mapping[fieldNode.name.value]; + newFieldNodes.push({ + ...fieldNode, + arguments: originalFieldNode.arguments.filter((arg): boolean => newArgs.includes(arg.name.value)), + }); + } else { + newFieldNodes.push(fieldNode); } - } - return selectionNode; - } - ); + }); + }); - return { ...selectionSetDef, selections }; + return newFieldNodes; + }; }; -}; +} diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 40d63a88da6..411d7fe3f38 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -5,9 +5,10 @@ import { GraphQLDirective, specifiedDirectives, extendSchema, + isObjectType, } from 'graphql'; -import { SchemaDirectiveVisitor, mergeDeep, IResolvers, pruneSchema } from '@graphql-tools/utils'; +import { SchemaDirectiveVisitor, mergeDeep, IResolvers, pruneSchema, IObjectTypeResolver } from '@graphql-tools/utils'; import { addResolversToSchema, @@ -19,7 +20,14 @@ import { extendResolversFromInterfaces, } from '@graphql-tools/schema'; -import { SubschemaConfig, isSubschemaConfig, Subschema, defaultMergedResolver } from '@graphql-tools/delegate'; +import { + Subschema, + SubschemaConfig, + defaultMergedResolver, + getMergedParent, + isExternalObject, + isSubschemaConfig, +} from '@graphql-tools/delegate'; import { IStitchSchemasOptions, SubschemaConfigTransform } from './types'; @@ -141,11 +149,13 @@ export function stitchSchemas>({ // We allow passing in an array of resolver maps, in which case we merge them const resolverMap: IResolvers = Array.isArray(resolvers) ? resolvers.reduce(mergeDeep, {}) : 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, @@ -235,3 +245,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 d34ebcd4e46..0dc57b13f42 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -4,16 +4,19 @@ import { Kind, SelectionSetNode, isObjectType, - isScalarType, getNamedType, GraphQLInterfaceType, SelectionNode, print, isInterfaceType, isLeafType, + isUnionType, + isInputObjectType, + FieldNode, + GraphQLField, } from 'graphql'; -import { parseSelectionSet, TypeMap, IResolvers, IFieldResolverOptions } from '@graphql-tools/utils'; +import { parseSelectionSet, TypeMap, IResolvers, IFieldResolverOptions, collectFields, GraphQLExecutionContext } from '@graphql-tools/utils'; import { MergedTypeResolver, Subschema, SubschemaConfig, MergedTypeInfo, StitchingInfo } from '@graphql-tools/delegate'; @@ -27,57 +30,11 @@ export function createStitchingInfo( mergeTypes?: boolean | Array | MergeTypeFilter ): StitchingInfo { const mergedTypes = createMergedTypes(typeCandidates, mergeTypes); - const selectionSetsByField: Record> = Object.create(null); - - Object.entries(mergedTypes).forEach(([typeName, mergedTypeInfo]) => { - if (mergedTypeInfo.selectionSets == null && mergedTypeInfo.fieldSelectionSets == null) { - return; - } - - selectionSetsByField[typeName] = Object.create(null); - - mergedTypeInfo.selectionSets.forEach((selectionSet, subschemaConfig) => { - const schema = subschemaConfig.transformedSchema; - const type = schema.getType(typeName) as GraphQLObjectType; - const fields = type.getFields(); - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; - const fieldType = getNamedType(field.type); - if (selectionSet && isLeafType(fieldType) && selectionSetContainsTopLevelField(selectionSet, fieldName)) { - return; - } - if (selectionSetsByField[typeName][fieldName] == null) { - selectionSetsByField[typeName][fieldName] = { - kind: Kind.SELECTION_SET, - selections: [parseSelectionSet('{ __typename }', { noLocation: true }).selections[0]], - }; - } - selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ - fieldName - ].selections.concat(selectionSet.selections); - }); - }); - - mergedTypeInfo.fieldSelectionSets.forEach(selectionSetFieldMap => { - Object.keys(selectionSetFieldMap).forEach(fieldName => { - if (selectionSetsByField[typeName][fieldName] == null) { - selectionSetsByField[typeName][fieldName] = { - kind: Kind.SELECTION_SET, - selections: [parseSelectionSet('{ __typename }', { noLocation: true }).selections[0]], - }; - } - selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ - fieldName - ].selections.concat(selectionSetFieldMap[fieldName].selections); - }); - }); - }); return { subschemaMap, - selectionSetsByType: undefined, - selectionSetsByField, - dynamicSelectionSetsByField: undefined, + fieldNodesByField: undefined, + dynamicFieldNodesByField: undefined, mergedTypes, }; } @@ -131,6 +88,7 @@ function createMergedTypes( if (mergedTypeConfig.selectionSet) { const selectionSet = parseSelectionSet(mergedTypeConfig.selectionSet, { noLocation: true }); + selectionSets.set(subschema, selectionSet); } @@ -199,6 +157,7 @@ function createMergedTypes( fieldSelectionSets, uniqueFields: Object.create({}), nonUniqueFields: Object.create({}), + subschemaFields: Object.create({}), resolvers, }; @@ -208,6 +167,7 @@ function createMergedTypes( } else { mergedTypes[typeName].nonUniqueFields[fieldName] = supportedBySubschemas[fieldName]; } + mergedTypes[typeName].subschemaFields[fieldName] = true; }); } } @@ -221,69 +181,138 @@ export function completeStitchingInfo( resolvers: IResolvers, schema: GraphQLSchema ): StitchingInfo { - const selectionSetsByType = Object.create(null); - [schema.getQueryType(), schema.getMutationType()].forEach(rootType => { - if (rootType) { - selectionSetsByType[rootType.name] = parseSelectionSet('{ __typename }', { noLocation: true }); + const selectionSetsByField: Record> = Object.create(null); + Object.entries(stitchingInfo.mergedTypes).forEach(([typeName, mergedTypeInfo]) => { + if (mergedTypeInfo.selectionSets == null && mergedTypeInfo.fieldSelectionSets == null) { + return; } + + selectionSetsByField[typeName] = Object.create(null); + + mergedTypeInfo.selectionSets.forEach((selectionSet, subschemaConfig) => { + const schema = subschemaConfig.transformedSchema; + const type = schema.getType(typeName) as GraphQLObjectType; + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + const fieldType = getNamedType(field.type); + if (selectionSet && isLeafType(fieldType) && selectionSetContainsTopLevelField(selectionSet, fieldName)) { + return; + } + + const typeSelectionSets = selectionSetsByField[typeName]; + if (typeSelectionSets[fieldName] == null) { + typeSelectionSets[fieldName] = { + kind: Kind.SELECTION_SET, + selections: [], + }; + } + + const fieldSelectionSet = selectionSetsByField[typeName][fieldName]; + + fieldSelectionSet.selections = fieldSelectionSet.selections.concat(selectionSet.selections); + }); + }); + + mergedTypeInfo.fieldSelectionSets.forEach(selectionSetFieldMap => { + Object.keys(selectionSetFieldMap).forEach(fieldName => { + const typeSelectionSets = selectionSetsByField[typeName]; + if (typeSelectionSets[fieldName] == null) { + typeSelectionSets[fieldName] = { + kind: Kind.SELECTION_SET, + selections: [], + }; + } + + const fieldSelectionSet = selectionSetsByField[typeName][fieldName]; + fieldSelectionSet.selections = fieldSelectionSet.selections.concat(selectionSetFieldMap[fieldName].selections); + }); + }); }); - const selectionSetsByField = stitchingInfo.selectionSetsByField; - const dynamicSelectionSetsByField = Object.create(null); + const dynamicFieldNodesByField:Record Array>>> = Object.create(null); Object.keys(resolvers).forEach(typeName => { - const type = resolvers[typeName]; - if (isScalarType(type)) { + const typeEntry = resolvers[typeName]; + const type = schema.getType(typeName); + if (isLeafType(type) || isUnionType(type) || isInputObjectType(type)) { return; } - Object.keys(type).forEach(fieldName => { - const field = type[fieldName] as IFieldResolverOptions; + + Object.keys(typeEntry).forEach(fieldName => { + const field = typeEntry[fieldName] as IFieldResolverOptions; if (field.selectionSet) { if (typeof field.selectionSet === 'function') { - if (!(typeName in dynamicSelectionSetsByField)) { - dynamicSelectionSetsByField[typeName] = Object.create(null); + if (!(typeName in dynamicFieldNodesByField)) { + dynamicFieldNodesByField[typeName] = Object.create(null); } - if (!(fieldName in dynamicSelectionSetsByField[typeName])) { - dynamicSelectionSetsByField[typeName][fieldName] = []; + if (!(fieldName in dynamicFieldNodesByField[typeName])) { + dynamicFieldNodesByField[typeName][fieldName] = []; } - dynamicSelectionSetsByField[typeName][fieldName].push(field.selectionSet); + const buildFieldNodeFn = field.selectionSet as ((schema: GraphQLSchema, field: GraphQLField) => (originalFieldNode: FieldNode) => Array); + const fieldNodeFn = buildFieldNodeFn(schema, type.getFields()[fieldName]); + dynamicFieldNodesByField[typeName][fieldName].push((fieldNode: FieldNode) => fieldNodeFn(fieldNode)); } else { const selectionSet = parseSelectionSet(field.selectionSet, { noLocation: true }); if (!(typeName in selectionSetsByField)) { selectionSetsByField[typeName] = Object.create(null); } - if (!(fieldName in selectionSetsByField[typeName])) { - selectionSetsByField[typeName][fieldName] = { + const typeSelectionSets = selectionSetsByField[typeName]; + if (!(fieldName in typeSelectionSets)) { + typeSelectionSets[fieldName] = { kind: Kind.SELECTION_SET, selections: [], }; } - selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ - fieldName - ].selections.concat(selectionSet.selections); + + const fieldSelectionSet = typeSelectionSets[fieldName]; + fieldSelectionSet.selections = fieldSelectionSet.selections.concat(selectionSet.selections); } } }); }); + const partialExecutionContext = ({ + schema, + variableValues: Object.create(null), + fragments: Object.create(null), + } as unknown) as GraphQLExecutionContext; + + const fieldNodesByField: Record>> = Object.create(null); Object.keys(selectionSetsByField).forEach(typeName => { + const typeFieldNodes: Record> = Object.create(null); + fieldNodesByField[typeName] = typeFieldNodes; + + const type = schema.getType(typeName) as GraphQLObjectType; const typeSelectionSets = selectionSetsByField[typeName]; Object.keys(typeSelectionSets).forEach(fieldName => { + const consolidatedSelections: Map = new Map(); const selectionSet = typeSelectionSets[fieldName]; - selectionSet.selections.forEach(selection => { - consolidatedSelections.set(print(selection), selection); - }); - selectionSet.selections = Array.from(consolidatedSelections.values()); + selectionSet.selections.forEach(selection => consolidatedSelections.set(print(selection), selection)); + + const responseKeys = collectFields( + partialExecutionContext, + type, + { + kind: Kind.SELECTION_SET, + selections: Array.from(consolidatedSelections.values()) + }, + Object.create(null), + Object.create(null) + ); + + const fieldNodes: Array = []; + typeFieldNodes[fieldName] = fieldNodes; + Object.values(responseKeys).forEach(nodes => fieldNodes.push(...nodes)); }); }); - stitchingInfo.selectionSetsByType = selectionSetsByType; - stitchingInfo.selectionSetsByField = selectionSetsByField; - stitchingInfo.dynamicSelectionSetsByField = dynamicSelectionSetsByField; + stitchingInfo.fieldNodesByField = fieldNodesByField; + stitchingInfo.dynamicFieldNodesByField = dynamicFieldNodesByField; return stitchingInfo; } diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index 1251afdd843..85d40959093 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -1,7 +1,6 @@ import { GraphQLNamedType, GraphQLSchema, - SelectionSetNode, FieldNode, GraphQLFieldConfig, GraphQLObjectType, @@ -10,6 +9,7 @@ import { GraphQLInputObjectType, GraphQLEnumValueConfig, GraphQLEnumType, + GraphQLField, } from 'graphql'; import { ITypeDefinitions } from '@graphql-tools/utils'; import { Subschema, SubschemaConfig } from '@graphql-tools/delegate'; @@ -108,6 +108,6 @@ export type OnTypeConflict> = ( declare module '@graphql-tools/utils' { interface IFieldResolverOptions { fragment?: string; - selectionSet?: string | ((node: FieldNode) => SelectionSetNode); + selectionSet?: string | ((schema: GraphQLSchema, field: GraphQLField) => (originalFieldNode: FieldNode) => Array); } } diff --git a/packages/stitch/tests/alternateStitchSchemas.test.ts b/packages/stitch/tests/alternateStitchSchemas.test.ts index 9514ca50157..f2a62a11a01 100644 --- a/packages/stitch/tests/alternateStitchSchemas.test.ts +++ b/packages/stitch/tests/alternateStitchSchemas.test.ts @@ -6,7 +6,6 @@ import { GraphQLScalarType, FieldNode, printSchema, - graphqlSync, assertValidSchema, GraphQLFieldConfig, isSpecifiedScalarType, @@ -210,16 +209,13 @@ describe('merge schemas through transforms', () => { }, Bookings_Booking: { property: { - selectionSet: () => ({ - kind: Kind.SELECTION_SET, - selections: [{ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: 'propertyId', - } - }] - }), + selectionSet: () => () => [{ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'propertyId', + } + }], resolve: (parent, _args, context, info) => delegateToSchema({ schema: propertySubschema, @@ -540,7 +536,7 @@ describe('default values', () => { }); describe('rename fields that implement interface fields', () => { - test('should work', () => { + test('should work', async () => { const originalItem = { id: '123', camel: "I'm a camel!", @@ -612,10 +608,10 @@ describe('rename fields that implement interface fields', () => { } `; - const originalResult = graphqlSync(originalSchema, originalQuery); + const originalResult = await graphql(originalSchema, originalQuery); expect(originalResult).toEqual({ data: { node: originalItem } }); - const newResult = graphqlSync(wrappedSchema, newQuery); + const newResult = await graphql(wrappedSchema, newQuery); expect(newResult).toEqual({ data: { _node: originalItem } }); }); }); @@ -874,7 +870,7 @@ type Query { }); describe('rename nested object fields with interfaces', () => { - test('should work', () => { + test('should work', async () => { const originalNode = { aList: [ { @@ -975,8 +971,8 @@ describe('rename nested object fields with interfaces', () => { } `; - const originalResult = graphqlSync(originalSchema, originalQuery); - const transformedResult = graphqlSync(transformedSchema, transformedQuery); + const originalResult = await graphql(originalSchema, originalQuery); + const transformedResult = await graphql(transformedSchema, transformedQuery); expect(originalResult).toEqual({ data: { node: originalNode } }); expect(transformedResult).toEqual({ diff --git a/packages/stitch/tests/fixtures/schemas.ts b/packages/stitch/tests/fixtures/schemas.ts index 191fb01c2ae..1dfaed5ea0d 100644 --- a/packages/stitch/tests/fixtures/schemas.ts +++ b/packages/stitch/tests/fixtures/schemas.ts @@ -21,10 +21,12 @@ import { IResolvers, ExecutionResult, mapAsyncIterator, + isAsyncIterable, + AsyncExecutor, } from '@graphql-tools/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { ExecutionParams, SubschemaConfig } from '@graphql-tools/delegate'; +import { ExecutionParams, Executor, SubschemaConfig } from '@graphql-tools/delegate'; export class CustomError extends GraphQLError { constructor(message: string, extensions: Record) { @@ -684,17 +686,20 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ resolvers: subscriptionResolvers, }); -function makeExecutorFromSchema(schema: GraphQLSchema) { - return async ({ document, variables, context }: ExecutionParams) => { - return (new ValueOrPromise(() => graphql( +function makeExecutorFromSchema(schema: GraphQLSchema): Executor { + return ({ document, variables, context }) => { + return new ValueOrPromise(() => graphql( schema, print(document), null, context, variables, - )) - .then(originalResult => JSON.parse(JSON.stringify(originalResult))) - .resolve()) as Promise> | ExecutionResult; + )).then((resultOrIterable: ExecutionResult | AsyncIterableIterator) => { + if (isAsyncIterable(resultOrIterable)) { + return mapAsyncIterator(resultOrIterable, originalResult => JSON.parse(JSON.stringify(originalResult))); + } + return JSON.parse(JSON.stringify(resultOrIterable)); + }).resolve(); }; } diff --git a/packages/stitch/tests/selectionSetArgs.test.ts b/packages/stitch/tests/selectionSetArgs.test.ts index 0397a21c0dc..c6dfbbc2d04 100644 --- a/packages/stitch/tests/selectionSetArgs.test.ts +++ b/packages/stitch/tests/selectionSetArgs.test.ts @@ -1,13 +1,37 @@ +import { makeExecutableSchema } from '@graphql-tools/schema'; import { parseSelectionSet } from '@graphql-tools/utils'; +import { FieldNode, GraphQLObjectType } from 'graphql'; import { forwardArgsToSelectionSet } from '../src'; describe('forwardArgsToSelectionSet', () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + users: [User] + } - const GATEWAY_FIELD = parseSelectionSet('{ posts(pageNumber: 1, perPage: 7) }').selections[0]; + type User { + id: ID + posts(pageNumber: Int, perPage: Int): [Post] + postIds(pageNumber: Int, perPage: Int): [ID] + } + + type Post { + id: ID + } + `, + }); + + const type = schema.getType('User') as GraphQLObjectType; + + const field = type.getFields()['posts']; + + const fieldNode = parseSelectionSet('{ posts(pageNumber: 1, perPage: 7) { id } }').selections[0] as FieldNode; test('passes all arguments to a hint selection set', () => { - const buildSelectionSet = forwardArgsToSelectionSet('{ postIds }'); - const result = buildSelectionSet(GATEWAY_FIELD).selections[0]; + const buildFieldNodeFn = forwardArgsToSelectionSet('{ postIds }'); + const fieldNodeFn = buildFieldNodeFn(schema, type, field); + const result = fieldNodeFn(fieldNode)[0]; expect(result.name.value).toEqual('postIds'); expect(result.arguments.length).toEqual(2); @@ -18,16 +42,17 @@ describe('forwardArgsToSelectionSet', () => { }); test('passes mapped arguments to a hint selection set', () => { - const buildSelectionSet = forwardArgsToSelectionSet('{ id postIds }', { postIds: ['pageNumber'] }); - const result = buildSelectionSet(GATEWAY_FIELD); + const buildFieldNodeFn = forwardArgsToSelectionSet('{ id postIds }', { postIds: ['pageNumber'] }); + const fieldNodeFn = buildFieldNodeFn(schema, type, field); + const result = fieldNodeFn(fieldNode); - expect(result.selections.length).toEqual(2); - expect(result.selections[0].name.value).toEqual('id'); - expect(result.selections[0].arguments.length).toEqual(0); + expect(result.length).toEqual(2); + expect(result[0].name.value).toEqual('id'); + expect(result[0].arguments.length).toEqual(0); - expect(result.selections[1].name.value).toEqual('postIds'); - expect(result.selections[1].arguments.length).toEqual(1); - expect(result.selections[1].arguments[0].name.value).toEqual('pageNumber'); - expect(result.selections[1].arguments[0].value.value).toEqual('1'); + expect(result[1].name.value).toEqual('postIds'); + expect(result[1].arguments.length).toEqual(1); + expect(result[1].arguments[0].name.value).toEqual('pageNumber'); + expect(result[1].arguments[0].value.value).toEqual('1'); }); }); diff --git a/website/docs/stitch-api.md b/website/docs/stitch-api.md index 47b7f1f090c..4dc98e258a7 100644 --- a/website/docs/stitch-api.md +++ b/website/docs/stitch-api.md @@ -78,5 +78,5 @@ import { forwardArgsToSelectionSet } from '@graphql-tools/stitch'; forwardArgsToSelectionSet( selectionSet: string, mapping?: Record -) => (field: FieldNode) => SelectionSetNode +) => (schema: GraphQLSchema, field: GraphQLField) => (originalFieldNode: FieldNode) => Array ``` From 97e8e39681dae960d4bf9a416eb6257bd52c4f65 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:14:30 +0300 Subject: [PATCH 08/49] remove dead code --- .../wrap/src/transforms/TransformInputObjectFields.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/wrap/src/transforms/TransformInputObjectFields.ts b/packages/wrap/src/transforms/TransformInputObjectFields.ts index f823a511a33..a30e8ba29f6 100644 --- a/packages/wrap/src/transforms/TransformInputObjectFields.ts +++ b/packages/wrap/src/transforms/TransformInputObjectFields.ts @@ -7,7 +7,6 @@ import { visit, visitWithTypeInfo, Kind, - FragmentDefinitionNode, GraphQLInputObjectType, GraphQLInputType, ObjectValueNode, @@ -71,15 +70,12 @@ export default class TransformInputObjectFields implements Transform { _transformationContext: Record ): Request { const variableValues = originalRequest.variables; - const fragments = Object.create(null); const operations: Array = []; originalRequest.document.definitions.forEach(def => { if ((def as OperationDefinitionNode).kind === Kind.OPERATION_DEFINITION) { operations.push(def as OperationDefinitionNode); - } else { - fragments[(def as FragmentDefinitionNode).name.value] = def; } }); @@ -118,11 +114,6 @@ export default class TransformInputObjectFields implements Transform { } }); - originalRequest.document.definitions - .filter(def => def.kind === Kind.FRAGMENT_DEFINITION) - .forEach(def => { - fragments[(def as FragmentDefinitionNode).name.value] = def; - }); const document = this.transformDocument( originalRequest.document, this.mapping, From bc5988e8e9d755853bc4f0665d2345f60ba4d7c0 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:18:17 +0300 Subject: [PATCH 09/49] format WrapFields transform --- packages/wrap/src/transforms/WrapFields.ts | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/wrap/src/transforms/WrapFields.ts b/packages/wrap/src/transforms/WrapFields.ts index ee989ae9b9d..6b7458178f4 100644 --- a/packages/wrap/src/transforms/WrapFields.ts +++ b/packages/wrap/src/transforms/WrapFields.ts @@ -51,7 +51,7 @@ export default class WrapFields implements Transform dehoistValue(value, context), + [outerTypeName]: (value, context: WrapFieldsTransformationContext) => + dehoistValue(value, wrappingTypeNames, context), }, (errors, context: WrapFieldsTransformationContext) => dehoistErrors(errors, context) ); @@ -279,28 +280,31 @@ function hoistFieldNodes({ return newFieldNodes; } -export function dehoistValue(originalValue: any, context: WrapFieldsTransformationContext): any { +export function dehoistValue( + originalValue: any, + wrappingTypeNames: Array, + context: WrapFieldsTransformationContext +): any { if (originalValue == null) { return originalValue; } const newValue = Object.create(null); - Object.keys(originalValue).forEach(alias => { + Object.keys(originalValue).forEach(responseKey => { let obj = newValue; - const path = context.paths[alias]; + const path = context.paths[responseKey]; if (path == null) { - newValue[alias] = originalValue[alias]; + newValue[responseKey] = originalValue[responseKey]; return; } const pathToField = path.pathToField; - const fieldAlias = path.alias; - pathToField.forEach(key => { - obj = obj[key] = obj[key] || Object.create(null); + pathToField.forEach((key, index) => { + obj = obj[key] = obj[key] ?? { __typename: wrappingTypeNames[index] }; }); - obj[fieldAlias] = originalValue[alias]; + obj[path.alias] = originalValue[responseKey]; }); return newValue; From 04bcba5ba6c47888733c288d757fcbb05986ab1d Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:18:44 +0300 Subject: [PATCH 10/49] no longer need to add __typename within transform, as will always be added later --- .../src/transforms/TransformCompositeFields.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/wrap/src/transforms/TransformCompositeFields.ts b/packages/wrap/src/transforms/TransformCompositeFields.ts index 537b3e20d92..2333bed5bb6 100644 --- a/packages/wrap/src/transforms/TransformCompositeFields.ts +++ b/packages/wrap/src/transforms/TransformCompositeFields.ts @@ -25,7 +25,6 @@ export default class TransformCompositeFields implements Transform { private transformedSchema: GraphQLSchema; private typeInfo: TypeInfo; private mapping: Record>; - private subscriptionTypeName: string; constructor( fieldTransformer: FieldTransformer, @@ -62,7 +61,6 @@ export default class TransformCompositeFields implements Transform { }, }); this.typeInfo = new TypeInfo(this.transformedSchema); - this.subscriptionTypeName = originalWrappingSchema.getSubscriptionType()?.name; return this.transformedSchema; } @@ -137,20 +135,6 @@ export default class TransformCompositeFields implements Transform { const newName = selection.name.value; - // See https://github.com/ardatan/graphql-tools/issues/2282 - if ( - (this.dataTransformer != null || this.errorsTransformer != null) && - (this.subscriptionTypeName == null || parentTypeName !== this.subscriptionTypeName) - ) { - newSelections.push({ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: '__typename', - }, - }); - } - let transformedSelection: SelectionNode | Array; if (this.fieldNodeTransformer == null) { transformedSelection = selection; From 09d1b302b4f7fb6868229bea9a1f2e314fe14278 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:20:52 +0300 Subject: [PATCH 11/49] generateProxyingResolvers should optionally take a transformedSchema so that wrapSchema can be aware of the transformedSchema stitchSchemas was previously aware of this via StitchingInfo --- packages/wrap/src/generateProxyingResolvers.ts | 15 ++++++++++----- packages/wrap/src/wrapSchema.ts | 5 ++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/wrap/src/generateProxyingResolvers.ts b/packages/wrap/src/generateProxyingResolvers.ts index c2e07c0d6f7..579d1073988 100644 --- a/packages/wrap/src/generateProxyingResolvers.ts +++ b/packages/wrap/src/generateProxyingResolvers.ts @@ -1,4 +1,4 @@ -import { GraphQLFieldResolver, GraphQLObjectType, GraphQLResolveInfo, OperationTypeNode } from 'graphql'; +import { GraphQLFieldResolver, GraphQLObjectType, GraphQLResolveInfo, GraphQLSchema, OperationTypeNode } from 'graphql'; import { getResponseKeyFromInfo } from '@graphql-tools/utils'; import { @@ -10,15 +10,19 @@ import { applySchemaTransforms, isExternalObject, getUnpathedErrors, + getReceiver, } from '@graphql-tools/delegate'; export function generateProxyingResolvers( - subschemaConfig: SubschemaConfig + subschemaConfig: SubschemaConfig, + transformedSchema?: GraphQLSchema, ): Record>> { const targetSchema = subschemaConfig.schema; const createProxyingResolver = subschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver; - const transformedSchema = applySchemaTransforms(targetSchema, subschemaConfig); + if (transformedSchema === undefined) { + transformedSchema = applySchemaTransforms(targetSchema, subschemaConfig); + } const operationTypes: Record = { query: targetSchema.getQueryType(), @@ -72,14 +76,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 receiver = getReceiver(parent, subschema); + return resolveExternalValue(parent[responseKey], unpathedErrors, subschema, context, info, receiver); } } } diff --git a/packages/wrap/src/wrapSchema.ts b/packages/wrap/src/wrapSchema.ts index 12f55f7bcde..6fb4b31ead8 100644 --- a/packages/wrap/src/wrapSchema.ts +++ b/packages/wrap/src/wrapSchema.ts @@ -13,12 +13,11 @@ import { generateProxyingResolvers } from './generateProxyingResolvers'; export function wrapSchema(subschemaConfig: SubschemaConfig): GraphQLSchema { const targetSchema = subschemaConfig.schema; + const transformedSchema = applySchemaTransforms(targetSchema, subschemaConfig); - const proxyingResolvers = generateProxyingResolvers(subschemaConfig); + const proxyingResolvers = generateProxyingResolvers(subschemaConfig, transformedSchema); const schema = createWrappingSchema(targetSchema, proxyingResolvers); - const transformedSchema = applySchemaTransforms(schema, subschemaConfig); - return applySchemaTransforms(schema, subschemaConfig, transformedSchema); } From 29f67e3c8ebadda400fae9d53c2be5e288dfe0f7 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:21:22 +0300 Subject: [PATCH 12/49] update introspectSchema types --- packages/wrap/src/introspect.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wrap/src/introspect.ts b/packages/wrap/src/introspect.ts index 08ef51fdf9c..a68aa3fb483 100644 --- a/packages/wrap/src/introspect.ts +++ b/packages/wrap/src/introspect.ts @@ -37,7 +37,9 @@ export function introspectSchema return new ValueOrPromise(() => (executor as Executor)({ document: parsedIntrospectionQuery, context, - })).then(introspection => getSchemaFromIntrospection(introspection)).resolve() as any; + })).then( + (introspection: ExecutionResult) => getSchemaFromIntrospection(introspection) + ).resolve() as any; } // Keep for backwards compatibility. Will be removed on next release. From 0f8c63ca12750cacbc43be351db2fee4b51c3bcc Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:21:45 +0300 Subject: [PATCH 13/49] update tests with __typename now that it is added everywhere --- packages/wrap/tests/makeRemoteExecutableSchema.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/wrap/tests/makeRemoteExecutableSchema.test.ts b/packages/wrap/tests/makeRemoteExecutableSchema.test.ts index 554d656108b..152ddae6e27 100644 --- a/packages/wrap/tests/makeRemoteExecutableSchema.test.ts +++ b/packages/wrap/tests/makeRemoteExecutableSchema.test.ts @@ -211,16 +211,19 @@ describe('respects buildSchema options', () => { expect(print(calls[0].document)).toEqual(`\ { fieldA + __typename } `); expect(print(calls[1].document)).toEqual(`\ { fieldB + __typename } `); expect(print(calls[2].document)).toEqual(`\ { field3 + __typename } `); }); From f26739bc99e4449ebb1ba083b5bb90e7dffa8107 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:22:04 +0300 Subject: [PATCH 14/49] update batch-execute package --- packages/batch-execute/package.json | 6 + .../src/createBatchingExecutor.ts | 62 +++++++--- .../batch-execute/src/getBatchingExecutor.ts | 8 +- packages/batch-execute/src/memoize.ts | 13 +- .../batch-execute/src/mergeExecutionParams.ts | 68 +++++++--- packages/batch-execute/src/split.ts | 116 ++++++++++++++++++ packages/batch-execute/src/splitResult.ts | 109 +++++++++++++++- .../tests/mergeExecutionParams.spec.ts | 48 ++++++++ packages/batch-execute/tests/split.spec.ts | 46 +++++++ .../batch-execute/tests/splitResult.spec.ts | 81 ++++++++++++ 10 files changed, 511 insertions(+), 46 deletions(-) create mode 100644 packages/batch-execute/src/split.ts create mode 100644 packages/batch-execute/tests/mergeExecutionParams.spec.ts create mode 100644 packages/batch-execute/tests/split.spec.ts create mode 100644 packages/batch-execute/tests/splitResult.spec.ts diff --git a/packages/batch-execute/package.json b/packages/batch-execute/package.json index 83d392e02fd..36b19adf867 100644 --- a/packages/batch-execute/package.json +++ b/packages/batch-execute/package.json @@ -27,6 +27,12 @@ "tslib": "~2.2.0", "value-or-promise": "1.0.8" }, + "devDependencies": { + "@graphql-tools/delegate": "^7.1.2", + "@graphql-tools/mock": "^8.1.1", + "@graphql-tools/schema": "^7.1.3", + "@graphql-tools/utils": "^7.7.3" + }, "publishConfig": { "access": "public", "directory": "dist" diff --git a/packages/batch-execute/src/createBatchingExecutor.ts b/packages/batch-execute/src/createBatchingExecutor.ts index b3ccbe1381c..2db1cbee23c 100644 --- a/packages/batch-execute/src/createBatchingExecutor.ts +++ b/packages/batch-execute/src/createBatchingExecutor.ts @@ -1,57 +1,79 @@ -import { getOperationAST } from 'graphql'; +import { getOperationAST, GraphQLSchema } from 'graphql'; import DataLoader from 'dataloader'; import { ValueOrPromise } from 'value-or-promise'; -import { ExecutionParams, Executor, ExecutionResult } from '@graphql-tools/utils'; +import { AsyncExecutionResult, ExecutionParams, Executor, ExecutionResult } from '@graphql-tools/utils'; import { mergeExecutionParams } from './mergeExecutionParams'; import { splitResult } from './splitResult'; export function createBatchingExecutor( executor: Executor, + targetSchema: GraphQLSchema, dataLoaderOptions?: DataLoader.Options, extensionsReducer?: (mergedExtensions: Record, executionParams: ExecutionParams) => Record ): Executor { const loader = new DataLoader( - createLoadFn(executor, extensionsReducer ?? defaultExtensionsReducer), + createLoadFn(executor, targetSchema, extensionsReducer ?? defaultExtensionsReducer), dataLoaderOptions ); return (executionParams: ExecutionParams) => loader.load(executionParams); } function createLoadFn( - executor: ({ document, context, variables, info }: ExecutionParams) => ExecutionResult | Promise, + executor: ({ + document, + context, + variables, + info, + }: ExecutionParams) => + | ExecutionResult + | AsyncIterableIterator + | Promise>, + targetSchema: GraphQLSchema, extensionsReducer: (mergedExtensions: Record, executionParams: ExecutionParams) => Record ) { - return async (execs: Array): Promise> => { - const execBatches: Array> = []; + return async ( + executionParamSet: Array + ): Promise< + Array< + | ExecutionResult + | AsyncIterableIterator + | Promise> + > + > => { + const batchedExecutionParamSets: Array> = []; let index = 0; - const exec = execs[index]; - let currentBatch: Array = [exec]; - execBatches.push(currentBatch); - const operationType = getOperationAST(exec.document, undefined).operation; - while (++index < execs.length) { - const currentOperationType = getOperationAST(execs[index].document, undefined).operation; + const executionParams = executionParamSet[index]; + let currentBatch: Array = [executionParams]; + batchedExecutionParamSets.push(currentBatch); + const operationType = getOperationAST(executionParams.document, undefined).operation; + while (++index < executionParamSet.length) { + const currentOperationType = getOperationAST(executionParamSet[index].document, undefined).operation; if (operationType === currentOperationType) { - currentBatch.push(execs[index]); + currentBatch.push(executionParamSet[index]); } else { - currentBatch = [execs[index]]; - execBatches.push(currentBatch); + currentBatch = [executionParamSet[index]]; + batchedExecutionParamSets.push(currentBatch); } } - const executionResults: Array> = []; - execBatches.forEach(execBatch => { - const mergedExecutionParams = mergeExecutionParams(execBatch, extensionsReducer); + const executionResults: Array>> = []; + batchedExecutionParamSets.forEach(batchedExecutionParamSet => { + const mergedExecutionParams = mergeExecutionParams(batchedExecutionParamSet, targetSchema, extensionsReducer); executionResults.push(new ValueOrPromise(() => executor(mergedExecutionParams))); }); return ValueOrPromise.all(executionResults).then(resultBatches => { - let results: Array = []; + const results: Array< + | ExecutionResult + | AsyncIterableIterator + | Promise> + > = []; resultBatches.forEach((resultBatch, index) => { - results = results.concat(splitResult(resultBatch, execBatches[index].length)); + results.push(...splitResult(resultBatch, batchedExecutionParamSets[index].length)); }); return results; }).resolve(); diff --git a/packages/batch-execute/src/getBatchingExecutor.ts b/packages/batch-execute/src/getBatchingExecutor.ts index ba267b0fd0c..580d6b761e8 100644 --- a/packages/batch-execute/src/getBatchingExecutor.ts +++ b/packages/batch-execute/src/getBatchingExecutor.ts @@ -2,13 +2,15 @@ import DataLoader from 'dataloader'; import { ExecutionParams, Executor } from '@graphql-tools/utils'; import { createBatchingExecutor } from './createBatchingExecutor'; -import { memoize2of4 } from './memoize'; +import { memoize2of5 } from './memoize'; +import { GraphQLSchema } from 'graphql'; -export const getBatchingExecutor = memoize2of4(function ( +export const getBatchingExecutor = memoize2of5(function ( _context: Record = self ?? window ?? global, executor: Executor, + targetSchema: GraphQLSchema, dataLoaderOptions?: DataLoader.Options, extensionsReducer?: (mergedExtensions: Record, executionParams: ExecutionParams) => Record ): Executor { - return createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer); + return createBatchingExecutor(executor, targetSchema, dataLoaderOptions, extensionsReducer); }); diff --git a/packages/batch-execute/src/memoize.ts b/packages/batch-execute/src/memoize.ts index 8b963a8ded5..352bf991b67 100644 --- a/packages/batch-execute/src/memoize.ts +++ b/packages/batch-execute/src/memoize.ts @@ -1,18 +1,19 @@ -export function memoize2of4< +export function memoize2of5< T1 extends Record, T2 extends Record, T3 extends any, T4 extends any, + T5 extends any, R extends any ->(fn: (A1: T1, A2: T2, A3: T3, A4: T4) => R): (A1: T1, A2: T2, A3: T3, A4: T4) => R { +>(fn: (A1: T1, A2: T2, A3: T3, A4: T4, A5: T5) => R): (A1: T1, A2: T2, A3: T3, A4: T4, A5: T5) => R { let cache1: WeakMap>; - function memoized(a1: T1, a2: T2, a3: T3, a4: T4) { + function memoized(a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) { if (!cache1) { cache1 = new WeakMap(); const cache2: WeakMap = new WeakMap(); cache1.set(a1, cache2); - const newValue = fn(a1, a2, a3, a4); + const newValue = fn(a1, a2, a3, a4, a5); cache2.set(a2, newValue); return newValue; } @@ -21,14 +22,14 @@ export function memoize2of4< if (!cache2) { cache2 = new WeakMap(); cache1.set(a1, cache2); - const newValue = fn(a1, a2, a3, a4); + const newValue = fn(a1, a2, a3, a4, a5); cache2.set(a2, newValue); return newValue; } const cachedValue = cache2.get(a2); if (cachedValue === undefined) { - const newValue = fn(a1, a2, a3, a4); + const newValue = fn(a1, a2, a3, a4, a5); cache2.set(a2, newValue); return newValue; } diff --git a/packages/batch-execute/src/mergeExecutionParams.ts b/packages/batch-execute/src/mergeExecutionParams.ts index 61b4184a634..ab31b633299 100644 --- a/packages/batch-execute/src/mergeExecutionParams.ts +++ b/packages/batch-execute/src/mergeExecutionParams.ts @@ -16,6 +16,7 @@ import { InlineFragmentNode, FieldNode, OperationTypeNode, + GraphQLSchema, } from 'graphql'; import { ExecutionParams } from '@graphql-tools/utils'; @@ -31,6 +32,7 @@ import { createPrefix } from './prefix'; * 2. Add unique aliases to all top-level query fields (including those on inline fragments) * 3. Prefix all variable definitions and variable usages * 4. Prefix names (and spreads) of fragments + * 5. Defer each set of top-level root fields. * * i.e transform: * [ @@ -44,20 +46,25 @@ import { createPrefix } from './prefix'; * $graphqlTools1_id: ID! * $graphqlTools2_id: ID! * ) { - * graphqlTools1_foo: foo, - * graphqlTools1_bar: bar(id: $graphqlTools1_id) - * ... on Query { - * graphqlTools1__baz: baz + * ... on Query @defer(label: "graphqlTools1_") { + * graphqlTools1_foo: foo, + * graphqlTools1_bar: bar(id: $graphqlTools1_id) + * ... on Query { + * graphqlTools1_baz: baz + * } * } - * graphqlTools1__foo: baz - * graphqlTools1__bar: bar(id: $graphqlTools1__id) - * ... on Query { - * graphqlTools1__baz: baz + * ... on Query @defer(label: "graphqlTools2_") { + * graphqlTools2_foo: baz + * graphqlTools2_bar: bar(id: $graphqlTools1_id) + * ... on Query { + * graphqlTools2_baz: baz + * } * } * } */ export function mergeExecutionParams( - execs: Array, + executionParamSets: Array, + targetSchema: GraphQLSchema, extensionsReducer: (mergedExtensions: Record, executionParams: ExecutionParams) => Record ): ExecutionParams { const mergedVariables: Record = Object.create(null); @@ -67,16 +74,47 @@ export function mergeExecutionParams( let mergedExtensions: Record = Object.create(null); let operation: OperationTypeNode; + executionParamSets.forEach((executionParams, index) => { + const prefix = createPrefix(index); - execs.forEach((executionParams, index) => { - const prefixedExecutionParams = prefixExecutionParams(createPrefix(index), executionParams); + const prefixedExecutionParams = prefixExecutionParams(prefix, executionParams); prefixedExecutionParams.document.definitions.forEach(def => { if (isOperationDefinition(def)) { operation = def.operation; - mergedSelections.push(...def.selectionSet.selections); + + const selections = targetSchema.getDirective('defer') + ? [ + { + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: (operation === 'query' ? targetSchema.getQueryType() : targetSchema.getMutationType()).name, + }, + }, + directives: [ + { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: 'defer', + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: def.selectionSet.selections, + }, + }, + ] + : def.selectionSet.selections; + + mergedSelections.push(...selections); mergedVariableDefinitions.push(...(def.variableDefinitions ?? [])); } + if (isFragmentDefinition(def)) { mergedFragmentDefinitions.push(def); } @@ -102,14 +140,14 @@ export function mergeExecutionParams( }, variables: mergedVariables, extensions: mergedExtensions, - context: execs[0].context, - info: execs[0].info, + context: executionParamSets[0].context, + info: executionParamSets[0].info, }; } function prefixExecutionParams(prefix: string, executionParams: ExecutionParams): ExecutionParams { let document = aliasTopLevelFields(prefix, executionParams.document); - const variableNames = Object.keys(executionParams.variables); + const variableNames = executionParams.variables !== undefined ? Object.keys(executionParams.variables) : []; if (variableNames.length === 0) { return { ...executionParams, document }; diff --git a/packages/batch-execute/src/split.ts b/packages/batch-execute/src/split.ts new file mode 100644 index 00000000000..aa5161f81c7 --- /dev/null +++ b/packages/batch-execute/src/split.ts @@ -0,0 +1,116 @@ +// adapted from: https://stackoverflow.com/questions/63543455/how-to-multicast-an-async-iterable +// and: https://gist.github.com/jed/cc1e949419d42e2cb26d7f2e1645864d +// and also: https://github.com/repeaterjs/repeater/issues/48#issuecomment-569134039 + +import { Push, Stop, Repeater } from '@repeaterjs/repeater'; + +type Splitter = (item: T) => [number | undefined, T]; + +export function split(asyncIterable: AsyncIterableIterator, n: number, splitter: Splitter) { + const iterator = asyncIterable[Symbol.asyncIterator](); + const returner = iterator.return?.bind(iterator) ?? undefined; + + const buffers: Array>> = Array(n); + for (let i = 0; i < n; i++) { + buffers[i] = []; + } + + if (returner) { + const set: Set = new Set(); + return buffers.map((buffer, index) => { + set.add(index); + return new Repeater(async (push, stop) => { + let earlyReturn: any; + stop.then(() => { + set.delete(index); + if (!set.size) { + earlyReturn = returner(); + } + }); + + await loop(push, stop, earlyReturn, buffer, buffers, iterator, splitter); + + await earlyReturn; + }); + }); + } + + return buffers.map( + buffer => + new Repeater(async (push, stop) => { + let earlyReturn: any; + stop.then(() => { + earlyReturn = returner ? returner() : true; + }); + + await loop(push, stop, earlyReturn, buffer, buffers, iterator, splitter); + + await earlyReturn; + }) + ); +} + +async function loop( + push: Push, + stop: Stop, + earlyReturn: Promise | any, + buffer: Array>, + buffers: Array>>, + iterator: AsyncIterator, + splitter: Splitter +): Promise { + /* eslint-disable no-unmodified-loop-condition */ + while (!earlyReturn) { + const iteration = await next(buffer, buffers, iterator, splitter); + + if (iteration === undefined) { + continue; + } + + if (iteration.done) { + stop(); + return iteration.value; + } + + await push(iteration.value); + } + /* eslint-enable no-unmodified-loop-condition */ +} + +async function next( + buffer: Array>, + buffers: Array>>, + iterator: AsyncIterator, + splitter: Splitter +): Promise | undefined> { + if (0 in buffer) { + return buffer.shift(); + } + + const iterationCandidate = await iterator.next(); + + let tee = true; + const value = iterationCandidate.value; + if (value !== undefined) { + const [iterationIndex, newValue] = splitter(value); + if (iterationIndex !== undefined) { + buffers[iterationIndex].push({ + ...iterationCandidate, + value: newValue, + }); + tee = false; + } + } + + if (tee) { + for (const b of buffers) { + b.push(iterationCandidate); + } + } + + if (0 in buffer) { + return buffer.shift(); + } + + return undefined; +} diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index fd2682a8e60..1b146380922 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -2,14 +2,119 @@ import { ExecutionResult, GraphQLError } from 'graphql'; -import { relocatedError } from '@graphql-tools/utils'; +import isPromise from 'is-promise'; + +import { AsyncExecutionResult, ExecutionPatchResult, isAsyncIterable, relocatedError } from '@graphql-tools/utils'; import { parseKey } from './prefix'; +import { split } from './split'; + +export function splitResult( + mergedResult: + | ExecutionResult + | AsyncIterableIterator + | Promise>, + numResults: number +): Array< + | ExecutionResult + | AsyncIterableIterator + | Promise> +> { + if (isPromise(mergedResult)) { + const result = mergedResult.then(r => splitExecutionResultOrAsyncIterableIterator(r, numResults)); + const splitResults: Array>> = []; + for (let i = 0; i < numResults; i++) { + splitResults.push(result.then(r => r[i])); + } + + return splitResults; + } + + return splitExecutionResultOrAsyncIterableIterator(mergedResult, numResults); +} + +export function splitExecutionResultOrAsyncIterableIterator( + mergedResult: ExecutionResult | AsyncIterableIterator, + numResults: number +): Array> { + if (isAsyncIterable(mergedResult)) { + return split(mergedResult, numResults, originalResult => { + const path = (originalResult as ExecutionPatchResult).path; + if (path && path.length) { + const { index, originalKey } = parseKey(path[0] as string); + const newPath = ([originalKey] as Array).concat(path.slice(1)); + + const newResult: ExecutionPatchResult = { + ...(originalResult as ExecutionPatchResult), + path: newPath, + }; + + const errors = originalResult.errors; + if (errors) { + const newErrors: Array = []; + errors.forEach(error => { + if (error.path) { + const parsedKey = parseKey(error.path[0] as string); + if (parsedKey) { + const { originalKey } = parsedKey; + const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); + newErrors.push(newError); + return; + } + } + + newErrors.push(error); + }); + newResult.errors = newErrors; + } + + return [index, newResult]; + } + + let resultIndex: number; + const newResult: ExecutionResult = { ...originalResult }; + const data = originalResult.data; + if (data) { + const newData = {}; + Object.keys(data).forEach(prefixedKey => { + const { index, originalKey } = parseKey(prefixedKey); + resultIndex = index; + newData[originalKey] = data[prefixedKey]; + }); + newResult.data = newData; + } + + const errors = originalResult.errors; + if (errors) { + const newErrors: Array = []; + errors.forEach(error => { + if (error.path) { + const parsedKey = parseKey(error.path[0] as string); + if (parsedKey) { + const { index, originalKey } = parsedKey; + resultIndex = index; + const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); + newErrors.push(newError); + return; + } + } + + newErrors.push(error); + }); + newResult.errors = newErrors; + } + + return [resultIndex, newResult] + }); + } + + return splitExecutionResult(mergedResult, numResults); +} /** * Split and transform result of the query produced by the `merge` function */ -export function splitResult(mergedResult: ExecutionResult, numResults: number): Array { +export function splitExecutionResult(mergedResult: ExecutionResult, numResults: number): Array { const splitResults: Array = []; for (let i = 0; i < numResults; i++) { splitResults.push({}); diff --git a/packages/batch-execute/tests/mergeExecutionParams.spec.ts b/packages/batch-execute/tests/mergeExecutionParams.spec.ts new file mode 100644 index 00000000000..f2225404590 --- /dev/null +++ b/packages/batch-execute/tests/mergeExecutionParams.spec.ts @@ -0,0 +1,48 @@ +import { parse } from 'graphql'; + +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { ExecutionParams } from '@graphql-tools/delegate'; + +import { mergeExecutionParams } from '../src/mergeExecutionParams'; + +describe('mergeExecutionParams', () => { + test('it works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + object: Object + } + type Object { + field1: String + field2: String + } + `, + }); + + const query1 = parse(`{ object { field1 } }`, { noLocation: true }); + const query2 = parse(`{ object { field2 } }`, { noLocation: true }); + + const mergedParams = mergeExecutionParams([{ document: query1 }, { document: query2 }], schema, () => ({})); + + const expectedMergedResult: ExecutionParams = { + document: parse(`{ + ... on Query @defer { + graphqlTools0_object: object { + field1 + } + } + ... on Query @defer { + graphqlTools1_object: object { + field2 + } + } + }`, { noLocation: true }), + variables: {}, + extensions: {}, + context: undefined, + info: undefined, + }; + + expect(expectedMergedResult).toMatchObject(mergedParams); + }); +}); diff --git a/packages/batch-execute/tests/split.spec.ts b/packages/batch-execute/tests/split.spec.ts new file mode 100644 index 00000000000..5e3fac7dac0 --- /dev/null +++ b/packages/batch-execute/tests/split.spec.ts @@ -0,0 +1,46 @@ +import { split } from '../src/split'; + +describe('split', () => { + test('it works sequentially', async () => { + const gen3 = async function* () { + for (let i = 0; i < 3; i++) { + yield i; + } + }(); + + const [one, two] = split(gen3, 2, (x) => [0, x + 5]); + + let results = []; + for await (const result of one) { + results.push(result); + } + expect(results).toEqual([5, 6, 7]); + + results = []; + for await (const result of two) { + results.push(result); + } + expect(results).toEqual([]); + }); + + test('it works in parallel', async () => { + const gen3 = async function* () { + for (let i = 0; i < 3; i++) { + yield i; + } + }(); + + const [one, two] = split(gen3, 2, (x) => [0, x + 5]); + + const oneResults = []; + const twoResults = []; + for (let i = 0; i < 3; i++) { + const results = await Promise.all([one.next(), two.next()]); + oneResults.push(results[0].value); + twoResults.push(results[1].value); + } + + expect(oneResults).toEqual([5, 6, 7]); + expect(twoResults).toEqual([undefined, undefined, undefined]); + }); +}); diff --git a/packages/batch-execute/tests/splitResult.spec.ts b/packages/batch-execute/tests/splitResult.spec.ts new file mode 100644 index 00000000000..973b61b1178 --- /dev/null +++ b/packages/batch-execute/tests/splitResult.spec.ts @@ -0,0 +1,81 @@ +import { graphql } from 'graphql'; + +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { addMocksToSchema } from '@graphql-tools/mock'; +import { isAsyncIterable } from '@graphql-tools/utils'; + +import { splitResult } from '../src/splitResult'; + +describe('splitResult', () => { + test('it works', async () => { + const schema = addMocksToSchema({ + schema: makeExecutableSchema({ + typeDefs: ` + type Query { + object: Object + } + type Object { + field1: String + field2: String + } + `, + }), + }); + + const mergedQuery = `{ + ... on Query @defer { + graphqlTools0_object: object { + field1 + } + } + ... on Query @defer { + graphqlTools1_object: object { + field2 + } + } + }`; + + const result = await graphql(schema, mergedQuery); + + const [zeroResult, oneResult] = splitResult(result, 2); + + const zeroResults = []; + if (isAsyncIterable(zeroResult)) { + for await (const payload of zeroResult) { + zeroResults.push(payload); + } + } + + const oneResults = []; + if (isAsyncIterable(oneResult)) { + for await (const payload of oneResult) { + oneResults.push(payload); + } + } + + expect(zeroResults).toEqual([{ + data: {}, + hasNext: true, + }, { + data: { + object: { + field1: 'Hello World', + }, + }, + path: [], + hasNext: true, + }]); + expect(oneResults).toEqual([{ + data: {}, + hasNext: true, + }, { + data: { + object: { + field2: 'Hello World', + }, + }, + path: [], + hasNext: false, + }]); + }); +}); From 9a4ce1a755175cb62f0cf19dbcb5dea2f09cb10b Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 22:22:25 +0300 Subject: [PATCH 15/49] remove dead type --- packages/batch-delegate/src/types.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/batch-delegate/src/types.ts b/packages/batch-delegate/src/types.ts index 8c30f74ecae..c9601b5541a 100644 --- a/packages/batch-delegate/src/types.ts +++ b/packages/batch-delegate/src/types.ts @@ -1,14 +1,6 @@ -import { FieldNode, GraphQLSchema } from 'graphql'; - import DataLoader from 'dataloader'; -import { IDelegateToSchemaOptions, SubschemaConfig } from '@graphql-tools/delegate'; - -// TODO: remove in next major release -export type DataLoaderCache = WeakMap< - ReadonlyArray, - WeakMap> ->; +import { IDelegateToSchemaOptions } from '@graphql-tools/delegate'; export type BatchDelegateFn, K = any> = ( batchDelegateOptions: BatchDelegateOptions From aee792cc7f41eccd1b36075ff2b33057f16c405e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 May 2021 23:11:21 +0300 Subject: [PATCH 16/49] use ValueOrPromise rather than isPromise --- packages/batch-execute/src/splitResult.ts | 25 +++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index 1b146380922..4f09bc56b1c 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -2,10 +2,10 @@ import { ExecutionResult, GraphQLError } from 'graphql'; -import isPromise from 'is-promise'; - import { AsyncExecutionResult, ExecutionPatchResult, isAsyncIterable, relocatedError } from '@graphql-tools/utils'; +import { ValueOrPromise } from 'value-or-promise'; + import { parseKey } from './prefix'; import { split } from './split'; @@ -20,17 +20,20 @@ export function splitResult( | AsyncIterableIterator | Promise> > { - if (isPromise(mergedResult)) { - const result = mergedResult.then(r => splitExecutionResultOrAsyncIterableIterator(r, numResults)); - const splitResults: Array>> = []; - for (let i = 0; i < numResults; i++) { - splitResults.push(result.then(r => r[i])); - } - - return splitResults; + const result = new ValueOrPromise(() => mergedResult).then(r => splitExecutionResultOrAsyncIterableIterator(r, numResults)); + + const splitResults: Array< + | ExecutionResult + | AsyncIterableIterator + | Promise> + > = []; + for (let i = 0; i < numResults; i++) { + splitResults.push(result.then(r => r[i]).resolve() as ExecutionResult + | AsyncIterableIterator + | Promise>); } - return splitExecutionResultOrAsyncIterableIterator(mergedResult, numResults); + return splitResults; } export function splitExecutionResultOrAsyncIterableIterator( From 2874c6c6a0656c97c8fc151614886c62430fe214 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 7 May 2021 14:22:30 +0300 Subject: [PATCH 17/49] refactor split --- packages/batch-execute/src/split.ts | 59 ++++++++++------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/packages/batch-execute/src/split.ts b/packages/batch-execute/src/split.ts index aa5161f81c7..f07e7fe0b34 100644 --- a/packages/batch-execute/src/split.ts +++ b/packages/batch-execute/src/split.ts @@ -8,46 +8,30 @@ type Splitter = (item: T) => [number | undefined, T]; export function split(asyncIterable: AsyncIterableIterator, n: number, splitter: Splitter) { const iterator = asyncIterable[Symbol.asyncIterator](); - const returner = iterator.return?.bind(iterator) ?? undefined; + const returner = iterator.return?.bind(iterator) ?? (() => {}); const buffers: Array>> = Array(n); for (let i = 0; i < n; i++) { buffers[i] = []; } - if (returner) { - const set: Set = new Set(); - return buffers.map((buffer, index) => { - set.add(index); - return new Repeater(async (push, stop) => { - let earlyReturn: any; - stop.then(() => { - set.delete(index); - if (!set.size) { - earlyReturn = returner(); - } - }); - - await loop(push, stop, earlyReturn, buffer, buffers, iterator, splitter); - - await earlyReturn; + const set: Set = new Set(); + return buffers.map((buffer, index) => { + set.add(index); + return new Repeater(async (push, stop) => { + let earlyReturn: any; + stop.then(() => { + set.delete(index); + if (!set.size) { + earlyReturn = returner(); + } }); - }); - } - return buffers.map( - buffer => - new Repeater(async (push, stop) => { - let earlyReturn: any; - stop.then(() => { - earlyReturn = returner ? returner() : true; - }); + await loop(push, stop, earlyReturn, buffer, buffers, iterator, splitter); - await loop(push, stop, earlyReturn, buffer, buffers, iterator, splitter); - - await earlyReturn; - }) - ); + await earlyReturn; + }); + }); } async function loop( @@ -83,8 +67,10 @@ async function next( iterator: AsyncIterator, splitter: Splitter ): Promise | undefined> { - if (0 in buffer) { - return buffer.shift(); + const existingIteration = buffer.shift(); + + if (existingIteration !== undefined) { + return existingIteration; } const iterationCandidate = await iterator.next(); @@ -93,6 +79,7 @@ async function next( const value = iterationCandidate.value; if (value !== undefined) { const [iterationIndex, newValue] = splitter(value); + if (iterationIndex !== undefined) { buffers[iterationIndex].push({ ...iterationCandidate, @@ -108,9 +95,5 @@ async function next( } } - if (0 in buffer) { - return buffer.shift(); - } - - return undefined; + return buffer.shift(); } From 35314950c605f3059b503c8fc4fffe02cc3fb722 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 7 May 2021 14:42:03 +0300 Subject: [PATCH 18/49] small refactor of splitResult --- packages/batch-execute/src/splitResult.ts | 129 +++++++++++----------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index 4f09bc56b1c..49059d3664c 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -41,83 +41,88 @@ export function splitExecutionResultOrAsyncIterableIterator( numResults: number ): Array> { if (isAsyncIterable(mergedResult)) { - return split(mergedResult, numResults, originalResult => { - const path = (originalResult as ExecutionPatchResult).path; - if (path && path.length) { - const { index, originalKey } = parseKey(path[0] as string); - const newPath = ([originalKey] as Array).concat(path.slice(1)); - - const newResult: ExecutionPatchResult = { - ...(originalResult as ExecutionPatchResult), - path: newPath, - }; - - const errors = originalResult.errors; - if (errors) { - const newErrors: Array = []; - errors.forEach(error => { - if (error.path) { - const parsedKey = parseKey(error.path[0] as string); - if (parsedKey) { - const { originalKey } = parsedKey; - const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); - newErrors.push(newError); - return; - } - } - - newErrors.push(error); - }); - newResult.errors = newErrors; + return split(mergedResult, numResults, originalResult => splitExecutionPatchResult(originalResult as ExecutionPatchResult)); + } + + return splitExecutionResult(mergedResult, numResults); +} + +function splitExecutionPatchResult(originalResult: ExecutionPatchResult): [number, ExecutionPatchResult] { + const path = originalResult.path; + if (path && path.length) { + const { index, originalKey } = parseKey(path[0] as string); + const newPath = ([originalKey] as Array).concat(path.slice(1)); + + const newResult: ExecutionPatchResult = { + ...originalResult, + path: newPath, + }; + + const errors = originalResult.errors; + if (errors) { + const newErrors: Array = []; + errors.forEach(error => { + if (error.path) { + const parsedKey = parseKey(error.path[0] as string); + if (parsedKey) { + const { originalKey } = parsedKey; + const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); + newErrors.push(newError); + return; + } } - return [index, newResult]; - } + newErrors.push(error); + }); + newResult.errors = newErrors; + } - let resultIndex: number; - const newResult: ExecutionResult = { ...originalResult }; - const data = originalResult.data; - if (data) { - const newData = {}; - Object.keys(data).forEach(prefixedKey => { - const { index, originalKey } = parseKey(prefixedKey); - resultIndex = index; - newData[originalKey] = data[prefixedKey]; - }); - newResult.data = newData; - } + return [index, newResult]; + } - const errors = originalResult.errors; - if (errors) { - const newErrors: Array = []; - errors.forEach(error => { - if (error.path) { - const parsedKey = parseKey(error.path[0] as string); - if (parsedKey) { - const { index, originalKey } = parsedKey; - resultIndex = index; - const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); - newErrors.push(newError); - return; - } - } + let resultIndex: number; + const newResult: ExecutionPatchResult = { ...originalResult }; + const data = originalResult.data; + if (data) { + const newData = {}; + Object.keys(data).forEach(prefixedKey => { + const { index, originalKey } = parseKey(prefixedKey); + resultIndex = index; + newData[originalKey] = data[prefixedKey]; + }); + newResult.data = newData; + } - newErrors.push(error); - }); - newResult.errors = newErrors; + const errors = originalResult.errors; + if (errors) { + const newErrors: Array = []; + errors.forEach(error => { + if (error.path) { + const parsedKey = parseKey(error.path[0] as string); + if (parsedKey) { + const { index, originalKey } = parsedKey; + resultIndex = index; + const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); + newErrors.push(newError); + return; + } } - return [resultIndex, newResult] + newErrors.push(error); }); + newResult.errors = newErrors; } - return splitExecutionResult(mergedResult, numResults); + return [resultIndex, newResult] } /** * Split and transform result of the query produced by the `merge` function + * Similar to above, but while an ExecutionPatchResult will only contain a + * data destined for a single target, an ExecutionResult may contain results + * for multiple targets, indexed by key. */ -export function splitExecutionResult(mergedResult: ExecutionResult, numResults: number): Array { +function splitExecutionResult(mergedResult: ExecutionResult, numResults: number): Array { const splitResults: Array = []; for (let i = 0; i < numResults; i++) { splitResults.push({}); From a9e946dda89d5b5106d1c3ecb77b1bc57f9a07e2 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 7 May 2021 14:49:32 +0300 Subject: [PATCH 19/49] remove unnecessary casts --- packages/delegate/src/delegateToSchema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 46b30f47982..753eb3a8d6b 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -261,6 +261,7 @@ function getExecutor(delegationContext: DelegationContext): Executor { executor = getBatchingExecutor( context, executor, + targetSchema, batchingOptions?.dataLoaderOptions, batchingOptions?.extensionsReducer ); From 6609312615b860c272c5e4684af79e1dae1b8797 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 7 May 2021 14:51:26 +0300 Subject: [PATCH 20/49] add missing dependency --- packages/delegate/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/delegate/package.json b/packages/delegate/package.json index a298d0e8e50..3b99ddf5d3c 100644 --- a/packages/delegate/package.json +++ b/packages/delegate/package.json @@ -28,6 +28,7 @@ "@graphql-tools/utils": "^7.7.1", "@repeaterjs/repeater": "^3.0.4", "dataloader": "2.0.0", + "is-promise": "4.0.0", "tslib": "~2.2.0", "value-or-promise": "1.0.8" }, From 0d6fe4924a2e31070c0b4f806a3542ebc57b36c1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 7 May 2021 15:26:14 +0300 Subject: [PATCH 21/49] fix(types): a little --- packages/delegate/src/delegateToSchema.ts | 9 +++++---- packages/links/src/linkToSubscriber.ts | 2 +- packages/loaders/url/src/index.ts | 2 +- packages/stitch/tests/fixtures/schemas.ts | 3 +-- packages/utils/src/executor.ts | 2 +- packages/wrap/src/introspect.ts | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 753eb3a8d6b..19a47f6ff9a 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -23,6 +23,7 @@ import { ExecutionParams, ExecutionResult, Executor, + Subscriber, isAsyncIterable, mapAsyncIterator, Subscriber, @@ -126,7 +127,7 @@ export function delegateRequest, TArgs = any>(opt ...processedRequest, context, info, - }).then((subscriptionResult: AsyncIterableIterator | ExecutionResult) => + }).then(subscriptionResult => handleSubscriptionResult(subscriptionResult, delegationContext, originalResult => transformer.transformResult(originalResult) ) @@ -286,13 +287,13 @@ const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValu })) as Executor; }); -function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record) { - return ({ document, context, variables, info }: ExecutionParams) => +function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record): Subscriber { + return (async ({ document, context, variables, info }: ExecutionParams) => subscribe({ schema, document, contextValue: context, variableValues: variables, rootValue: rootValue ?? info?.rootValue, - }) as any; + })) as Subscriber; } diff --git a/packages/links/src/linkToSubscriber.ts b/packages/links/src/linkToSubscriber.ts index 0b66c661b1c..e1d3b7f47fa 100644 --- a/packages/links/src/linkToSubscriber.ts +++ b/packages/links/src/linkToSubscriber.ts @@ -5,7 +5,7 @@ import { Subscriber, ExecutionParams, ExecutionResult, observableToAsyncIterable export const linkToSubscriber = (link: ApolloLink): Subscriber => async ( params: ExecutionParams -): Promise | AsyncIterator>> => { +): Promise | AsyncIterableIterator>> => { const { document, variables, extensions, context, info } = params; return observableToAsyncIterable>( execute(link, { diff --git a/packages/loaders/url/src/index.ts b/packages/loaders/url/src/index.ts index 0596dceb172..0b3e64d896e 100644 --- a/packages/loaders/url/src/index.ts +++ b/packages/loaders/url/src/index.ts @@ -414,7 +414,7 @@ export class UrlLoader implements DocumentLoader { query: document, variables, }) - ) as AsyncIterator>; + ) as AsyncIterableIterator>; }; } diff --git a/packages/stitch/tests/fixtures/schemas.ts b/packages/stitch/tests/fixtures/schemas.ts index 1dfaed5ea0d..2d12cc65bdf 100644 --- a/packages/stitch/tests/fixtures/schemas.ts +++ b/packages/stitch/tests/fixtures/schemas.ts @@ -22,7 +22,6 @@ import { ExecutionResult, mapAsyncIterator, isAsyncIterable, - AsyncExecutor, } from '@graphql-tools/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -694,7 +693,7 @@ function makeExecutorFromSchema(schema: GraphQLSchema): Executor { null, context, variables, - )).then((resultOrIterable: ExecutionResult | AsyncIterableIterator) => { + )).then(resultOrIterable => { if (isAsyncIterable(resultOrIterable)) { return mapAsyncIterator(resultOrIterable, originalResult => JSON.parse(JSON.stringify(originalResult))); } diff --git a/packages/utils/src/executor.ts b/packages/utils/src/executor.ts index 03bfced5f4f..311fdda6083 100644 --- a/packages/utils/src/executor.ts +++ b/packages/utils/src/executor.ts @@ -39,4 +39,4 @@ export type Subscriber> = < TContext extends TBaseContext = TBaseContext >( params: ExecutionParams -) => Promise> | ExecutionResult>; +) => Promise> | ExecutionResult>; diff --git a/packages/wrap/src/introspect.ts b/packages/wrap/src/introspect.ts index a68aa3fb483..38703111e6c 100644 --- a/packages/wrap/src/introspect.ts +++ b/packages/wrap/src/introspect.ts @@ -28,11 +28,11 @@ function getSchemaFromIntrospection(introspectionResult: ExecutionResult( +export function introspectSchema( executor: TExecutor, context?: Record, options?: IntrospectionOptions -): TExecutor extends AsyncExecutor ? Promise : GraphQLSchema { +): TExecutor extends SyncExecutor ? GraphQLSchema : TExecutor extends AsyncExecutor ? Promise : Promise | GraphQLSchema { const parsedIntrospectionQuery: DocumentNode = parse(getIntrospectionQuery(options)); return new ValueOrPromise(() => (executor as Executor)({ document: parsedIntrospectionQuery, From c50a81956f20ba448f11dad6bff888ff5be3a299 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sat, 8 May 2021 23:20:48 +0300 Subject: [PATCH 22/49] allow more than one type of Receiver --- packages/delegate/src/{Receiver.ts => InitialReceiver.ts} | 4 ++-- packages/delegate/src/delegateToSchema.ts | 4 ++-- packages/delegate/src/externalObjects.ts | 3 +-- packages/delegate/src/externalValues.ts | 3 +-- packages/delegate/src/resolveExternalValue.ts | 3 +-- packages/delegate/src/types.ts | 5 ++++- 6 files changed, 11 insertions(+), 11 deletions(-) rename packages/delegate/src/{Receiver.ts => InitialReceiver.ts} (98%) diff --git a/packages/delegate/src/Receiver.ts b/packages/delegate/src/InitialReceiver.ts similarity index 98% rename from packages/delegate/src/Receiver.ts rename to packages/delegate/src/InitialReceiver.ts index 85242731ba7..2d3ca4c92f2 100644 --- a/packages/delegate/src/Receiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -17,13 +17,13 @@ import { Repeater, Stop } from '@repeaterjs/repeater'; import { AsyncExecutionResult, getResponseKeyFromInfo } from '@graphql-tools/utils'; -import { DelegationContext, ExternalObject } from './types'; +import { DelegationContext, ExternalObject, Receiver } from './types'; import { getReceiver, getSubschema, getUnpathedErrors, mergeExternalObjects } from './externalObjects'; import { resolveExternalValue } from './resolveExternalValue'; import { externalValueFromResult, externalValueFromPatchResult } from './externalValues'; import { ExpectantStore } from './expectantStore'; -export class Receiver { +export class InitialReceiver implements Receiver { private readonly asyncIterable: AsyncIterable; private readonly delegationContext: DelegationContext; private readonly fieldName: string; diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 19a47f6ff9a..efdfd7f04ff 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -41,7 +41,7 @@ import { Subschema } from './Subschema'; import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; import { Transformer } from './Transformer'; import { memoize2 } from './memoize'; -import { Receiver } from './Receiver'; +import { InitialReceiver } from './InitialReceiver'; import { externalValueFromResult } from './externalValues'; export function delegateToSchema, TArgs = any>( @@ -140,7 +140,7 @@ function handleExecutionResult( resultTransformer: (originalResult: ExecutionResult) => ExecutionResult ): any { if (isAsyncIterable(executionResult)) { - const receiver = new Receiver(executionResult, delegationContext, resultTransformer); + const receiver = new InitialReceiver(executionResult, delegationContext, resultTransformer); return receiver.getInitialResult(); } diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts index e8375eda74f..b0cbb5452ae 100644 --- a/packages/delegate/src/externalObjects.ts +++ b/packages/delegate/src/externalObjects.ts @@ -10,7 +10,7 @@ import { import { relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils'; -import { SubschemaConfig, ExternalObject } from './types'; +import { SubschemaConfig, ExternalObject, Receiver } from './types'; import { OBJECT_SUBSCHEMA_SYMBOL, INITIAL_POSSIBLE_FIELDS, @@ -19,7 +19,6 @@ import { UNPATHED_ERRORS_SYMBOL, RECEIVER_MAP_SYMBOL, } from './symbols'; -import { Receiver } from './Receiver'; import { isSubschemaConfig } from './subschemaConfig'; import { Subschema } from './Subschema'; diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index 1c7e608e77d..18880d60116 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -4,9 +4,8 @@ import AggregateError from '@ardatan/aggregate-error'; import { ExecutionPatchResult, ExecutionResult, relocatedError } from '@graphql-tools/utils'; -import { DelegationContext } from './types'; +import { DelegationContext, Receiver } from './types'; import { resolveExternalValue } from './resolveExternalValue'; -import { Receiver } from './Receiver'; export function externalValueFromResult( originalResult: ExecutionResult, diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 71230fd1817..3b1c4c6037a 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -13,9 +13,8 @@ import { import AggregateError from '@ardatan/aggregate-error'; -import { SubschemaConfig } from './types'; +import { Receiver, SubschemaConfig } from './types'; import { annotateExternalObject, isExternalObject } from './externalObjects'; -import { Receiver } from './Receiver'; export function resolveExternalValue( result: any, diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index ff323ef04d0..c74aa32bcfd 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -27,7 +27,6 @@ import { } from './symbols'; import { Subschema } from './Subschema'; -import { Receiver } from './Receiver'; export type SchemaTransform = ( originalWrappingSchema: GraphQLSchema, @@ -201,6 +200,10 @@ export interface StitchingInfo> { mergedTypes: Record>; } +export interface Receiver { + request: (info: GraphQLResolveInfo) => Promise; +} + export interface ExternalObject> { __typename: string; [key: string]: any; From 336a4107ddab1fd445ecdadc7bd945abf9f31792 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 9 May 2021 22:30:04 +0300 Subject: [PATCH 23/49] when possible, update the receiver synchronously rather than requesting data from it not sure why (yet?), but it also helps some tests pass --- packages/delegate/src/InitialReceiver.ts | 96 +++++++++++-------- .../delegate/src/defaultMergedResolver.ts | 11 ++- packages/delegate/src/fieldShouldStream.ts | 6 ++ packages/delegate/src/types.ts | 1 + 4 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 packages/delegate/src/fieldShouldStream.ts diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index 2d3ca4c92f2..1805d37b66a 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -22,6 +22,7 @@ import { getReceiver, getSubschema, getUnpathedErrors, mergeExternalObjects } fr import { resolveExternalValue } from './resolveExternalValue'; import { externalValueFromResult, externalValueFromPatchResult } from './externalValues'; import { ExpectantStore } from './expectantStore'; +import { fieldShouldStream } from './fieldShouldStream'; export class InitialReceiver implements Receiver { private readonly asyncIterable: AsyncIterable; @@ -80,6 +81,39 @@ export class InitialReceiver implements Receiver { return initialResult; } + public update(parent: ExternalObject, info: GraphQLResolveInfo): any { + const path = responsePathAsArray(info.path).slice(this.initialResultDepth); + const pathKey = path.join('.'); + const responseKey = path.slice().pop() as string; + const data = parent[responseKey]; + + return this._update(parent, info, pathKey, responseKey, data); + } + + private _update( + parent: ExternalObject, + info: GraphQLResolveInfo, + pathKey: string, + responseKey: string, + data: any, + ): any { + const unpathedErrors = getUnpathedErrors(parent); + const subschema = getSubschema(parent, responseKey); + const receiver = getReceiver(parent, subschema); + const newExternalValue = resolveExternalValue(data, unpathedErrors, subschema, this.context, info, receiver); + this.onNewExternalValue( + pathKey, + newExternalValue, + isCompositeType(getNamedType(info.returnType)) + ? { + kind: Kind.SELECTION_SET, + selections: [].concat(...info.fieldNodes.map(fieldNode => fieldNode.selectionSet.selections)), + } + : undefined + ); + return newExternalValue; + } + public request(info: GraphQLResolveInfo): Promise { const path = responsePathAsArray(info.path).slice(this.initialResultDepth); const pathKey = path.join('.'); @@ -96,7 +130,7 @@ export class InitialReceiver implements Receiver { path: Array, pathKey: string, infos: ReadonlyArray - ): Promise { + ): Promise> { const parentPath = path.slice(); const responseKey = parentPath.pop() as string; const parentKey = parentPath.join('.'); @@ -124,48 +158,37 @@ export class InitialReceiver implements Receiver { const data = parent[responseKey]; if (data !== undefined) { - const unpathedErrors = getUnpathedErrors(parent); - const subschema = getSubschema(parent, responseKey); - const receiver = getReceiver(parent, subschema); - this.onNewExternalValue( - pathKey, - resolveExternalValue(data, unpathedErrors, subschema, this.context, combinedInfo, receiver), - isCompositeType(getNamedType(combinedInfo.returnType)) - ? { - kind: Kind.SELECTION_SET, - selections: [].concat(...combinedInfo.fieldNodes.map(fieldNode => fieldNode.selectionSet.selections)), - } - : undefined - ); + this._update(parent, combinedInfo, pathKey, responseKey, data); } if (fieldShouldStream(combinedInfo)) { - return infos.map( - () => - new Repeater(async (push, stop) => { - const initialValues = ((await this.cache.request(pathKey)) as unknown) as Array; - initialValues.forEach(async value => push(value)); + return infos.map(() => this._stream(pathKey)); + } - let index = initialValues.length; + const externalValue = await this.cache.request(pathKey); + return new Array(infos.length).fill(externalValue); + } - let stopped = false; - stop.then(() => (stopped = true)); + private _stream(pathKey: string): AsyncIterator { + return new Repeater(async (push, stop) => { + const initialValues = ((await this.cache.request(pathKey)) as unknown) as Array; - this.stoppers.push(stop); + let stopped = false; + stop.then(() => (stopped = true)); + this.stoppers.push(stop); - const next = () => this.cache.request(`${pathKey}.${index++}`); + let index = 0; - /* eslint-disable no-unmodified-loop-condition */ - while (!stopped) { - await push(next()); - } - /* eslint-disable no-unmodified-loop-condition */ - }) - ); - } + /* eslint-disable no-unmodified-loop-condition */ + while (!stopped && index < initialValues.length) { + await push(initialValues[index++]); + } - const externalValue = await this.cache.request(pathKey); - return new Array(infos.length).fill(externalValue); + while (!stopped) { + await push(this.cache.request(`${pathKey}.${index++}`)); + } + /* eslint-disable no-unmodified-loop-condition */ + }); } private async _iterate(): Promise { @@ -328,8 +351,3 @@ export class InitialReceiver implements Receiver { } } } - -function fieldShouldStream(info: GraphQLResolveInfo): boolean { - const directives = info.fieldNodes[0]?.directives; - return directives !== undefined && directives.some(directive => directive.name.value === 'stream'); -} diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index 25a8bbb4e36..0b68c9c9dc1 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -14,6 +14,7 @@ import { } from './externalObjects'; import { getMergedParent } from './getMergedParent'; +import { fieldShouldStream } from './fieldShouldStream'; /** * Resolver that knows how to: @@ -64,11 +65,19 @@ function resolveField( const fieldSubschema = getSubschema(parent, responseKey); const receiver = getReceiver(parent, fieldSubschema); + const data = parent[responseKey]; if (receiver !== undefined) { + if (fieldShouldStream(info)) { + return receiver.request(info); + } + + if (data !== undefined) { + return receiver.update(parent, info); + } + return receiver.request(info); } - const data = parent[responseKey]; if (data !== undefined) { const unpathedErrors = getUnpathedErrors(parent); return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info, receiver); diff --git a/packages/delegate/src/fieldShouldStream.ts b/packages/delegate/src/fieldShouldStream.ts new file mode 100644 index 00000000000..91d3be819b4 --- /dev/null +++ b/packages/delegate/src/fieldShouldStream.ts @@ -0,0 +1,6 @@ +import { GraphQLResolveInfo } from 'graphql'; + +export function fieldShouldStream(info: GraphQLResolveInfo): boolean { + const directives = info.fieldNodes[0]?.directives; + return directives !== undefined && directives.some(directive => directive.name.value === 'stream'); +} diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index c74aa32bcfd..ba0710e6047 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -202,6 +202,7 @@ export interface StitchingInfo> { export interface Receiver { request: (info: GraphQLResolveInfo) => Promise; + update: (parent: ExternalObject, info: GraphQLResolveInfo) => any; } export interface ExternalObject> { From cbb303c274ce5eebc95210cb40d71cc33ddd71b2 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 11 May 2021 21:27:10 +0300 Subject: [PATCH 24/49] fix split --- packages/batch-execute/src/split.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/batch-execute/src/split.ts b/packages/batch-execute/src/split.ts index f07e7fe0b34..9a72a8d0b62 100644 --- a/packages/batch-execute/src/split.ts +++ b/packages/batch-execute/src/split.ts @@ -52,8 +52,11 @@ async function loop( } if (iteration.done) { + if (iteration.value !== undefined) { + await push(iteration.value); + } stop(); - return iteration.value; + return; } await push(iteration.value); From c08360015306daa96b96068039e011ab16b44525 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 11 May 2021 21:28:01 +0300 Subject: [PATCH 25/49] remove logged payloads --- packages/delegate/src/InitialReceiver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index 1805d37b66a..4b9c5dbefe6 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -66,10 +66,8 @@ export class InitialReceiver implements Receiver { public async getInitialResult(): Promise { let initialResult: any; - const payloads: Array = []; for await (const payload of this.asyncIterable) { initialResult = externalValueFromResult(this.resultTransformer(payload), this.delegationContext, this); - payloads.push(payload); if (initialResult != null) { break; } From 30533c61d18e7a9fb9028554208f96bf269c99f1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 11 May 2021 21:31:01 +0300 Subject: [PATCH 26/49] implement mapAsyncIterator with repeaters Implementation adapted from: https://github.com/repeaterjs/repeater/issues/48#issuecomment-569134039 so that all payloads will be delivered in the original order --- packages/utils/package.json | 1 + packages/utils/src/mapAsyncIterator.ts | 102 ++++++++++++++----------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index 17e7217ef73..7e60483f3e3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@ardatan/aggregate-error": "0.0.6", + "@repeaterjs/repeater": "^3.0.4", "camel-case": "4.1.2", "tslib": "~2.2.0" }, diff --git a/packages/utils/src/mapAsyncIterator.ts b/packages/utils/src/mapAsyncIterator.ts index 67adfa2ffda..8a28c971ea0 100644 --- a/packages/utils/src/mapAsyncIterator.ts +++ b/packages/utils/src/mapAsyncIterator.ts @@ -1,59 +1,71 @@ /** - * Given an AsyncIterable and a callback function, return an AsyncIterator + * Given an AsyncIterator and a callback function, return an AsyncIterator * which produces values mapped via calling the callback function. + * + * Implementation adapted from: + * https://github.com/repeaterjs/repeater/issues/48#issuecomment-569134039 + * so that all payloads will be delivered in the original order */ + +import { Push, Stop, Repeater } from '@repeaterjs/repeater'; + export function mapAsyncIterator( iterator: AsyncIterator, - callback: (value: T) => Promise | U, - rejectCallback?: any + mapValue: (value: T) => Promise | U, ): AsyncIterableIterator { - let $return: any; - let abruptClose: any; - - if (typeof iterator.return === 'function') { - $return = iterator.return; - abruptClose = (error: any) => { - const rethrow = () => Promise.reject(error); - return $return.call(iterator).then(rethrow, rethrow); - }; - } + const returner = iterator.return?.bind(iterator) ?? (() => {}); - function mapResult(result: any) { - return result.done ? result : asyncMapValue(result.value, callback).then(iteratorResult, abruptClose); - } + return new Repeater(async (push, stop) => { + let earlyReturn: any; + stop.then(() => { + earlyReturn = returner(); + }); - let mapReject: any; - if (rejectCallback) { - // Capture rejectCallback to ensure it cannot be null. - const reject = rejectCallback; - mapReject = (error: any) => asyncMapValue(error, reject).then(iteratorResult, abruptClose); - } + await loop(push, stop, earlyReturn, iterator, mapValue); - return { - next() { - return iterator.next().then(mapResult, mapReject); - }, - return() { - return $return - ? $return.call(iterator).then(mapResult, mapReject) - : Promise.resolve({ value: undefined, done: true }); - }, - throw(error: any) { - if (typeof iterator.throw === 'function') { - return iterator.throw(error).then(mapResult, mapReject); - } - return Promise.reject(error).catch(abruptClose); - }, - [Symbol.asyncIterator]() { - return this; - }, - }; + await earlyReturn; + }); } -function asyncMapValue(value: T, callback: (value: T) => Promise | U): Promise { - return new Promise(resolve => resolve(callback(value))); +async function loop( + push: Push, + stop: Stop, + earlyReturn: Promise | any, + iterator: AsyncIterator, + mapValue: (value: T) => Promise | U, +): Promise { + /* eslint-disable no-unmodified-loop-condition */ + while (!earlyReturn) { + const iteration = await next(iterator, mapValue); + + if (iteration.done) { + if (iteration.value !== undefined) { + await push(iteration.value); + } + stop(); + return; + } + + await push(iteration.value); + } + /* eslint-enable no-unmodified-loop-condition */ } -function iteratorResult(value: T): IteratorResult { - return { value, done: false }; +async function next( + iterator: AsyncIterator, + mapValue: (value: T) => Promise | U, +): Promise> { + const iterationCandidate = await iterator.next(); + + const value = iterationCandidate.value; + if (value === undefined) { + return iterationCandidate as IteratorResult; + } + + const newValue = await mapValue(iterationCandidate.value); + + return { + ...iterationCandidate, + value: newValue, + }; } From b46a2553dd91ecaa5796fb691a4b065821b12d3e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 11 May 2021 21:36:01 +0300 Subject: [PATCH 27/49] rename split --- .../batch-execute/src/{split.ts => splitAsyncIterator.ts} | 3 +-- packages/batch-execute/src/splitResult.ts | 4 ++-- .../tests/{split.spec.ts => splitAsyncIterator.spec.ts} | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) rename packages/batch-execute/src/{split.ts => splitAsyncIterator.ts} (95%) rename packages/batch-execute/tests/{split.spec.ts => splitAsyncIterator.spec.ts} (79%) diff --git a/packages/batch-execute/src/split.ts b/packages/batch-execute/src/splitAsyncIterator.ts similarity index 95% rename from packages/batch-execute/src/split.ts rename to packages/batch-execute/src/splitAsyncIterator.ts index 9a72a8d0b62..eafff5fca8e 100644 --- a/packages/batch-execute/src/split.ts +++ b/packages/batch-execute/src/splitAsyncIterator.ts @@ -6,8 +6,7 @@ import { Push, Stop, Repeater } from '@repeaterjs/repeater'; type Splitter = (item: T) => [number | undefined, T]; -export function split(asyncIterable: AsyncIterableIterator, n: number, splitter: Splitter) { - const iterator = asyncIterable[Symbol.asyncIterator](); +export function splitAsyncIterator(iterator: AsyncIterator, n: number, splitter: Splitter) { const returner = iterator.return?.bind(iterator) ?? (() => {}); const buffers: Array>> = Array(n); diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index 49059d3664c..5975bb715c1 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -7,7 +7,7 @@ import { AsyncExecutionResult, ExecutionPatchResult, isAsyncIterable, relocatedE import { ValueOrPromise } from 'value-or-promise'; import { parseKey } from './prefix'; -import { split } from './split'; +import { splitAsyncIterator } from './splitAsyncIterator'; export function splitResult( mergedResult: @@ -41,7 +41,7 @@ export function splitExecutionResultOrAsyncIterableIterator( numResults: number ): Array> { if (isAsyncIterable(mergedResult)) { - return split(mergedResult, numResults, originalResult => splitExecutionPatchResult(originalResult as ExecutionPatchResult)); + return splitAsyncIterator(mergedResult, numResults, originalResult => splitExecutionPatchResult(originalResult as ExecutionPatchResult)); } return splitExecutionResult(mergedResult, numResults); diff --git a/packages/batch-execute/tests/split.spec.ts b/packages/batch-execute/tests/splitAsyncIterator.spec.ts similarity index 79% rename from packages/batch-execute/tests/split.spec.ts rename to packages/batch-execute/tests/splitAsyncIterator.spec.ts index 5e3fac7dac0..be4de47d004 100644 --- a/packages/batch-execute/tests/split.spec.ts +++ b/packages/batch-execute/tests/splitAsyncIterator.spec.ts @@ -1,6 +1,6 @@ -import { split } from '../src/split'; +import { splitAsyncIterator } from '../src/splitAsyncIterator'; -describe('split', () => { +describe('splitAsyncIterator', () => { test('it works sequentially', async () => { const gen3 = async function* () { for (let i = 0; i < 3; i++) { @@ -8,7 +8,7 @@ describe('split', () => { } }(); - const [one, two] = split(gen3, 2, (x) => [0, x + 5]); + const [one, two] = splitAsyncIterator(gen3, 2, (x) => [0, x + 5]); let results = []; for await (const result of one) { @@ -30,7 +30,7 @@ describe('split', () => { } }(); - const [one, two] = split(gen3, 2, (x) => [0, x + 5]); + const [one, two] = splitAsyncIterator(gen3, 2, (x) => [0, x + 5]); const oneResults = []; const twoResults = []; From 1c9e3215c280443343bdb35aa932d2d47dcded63 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 11 May 2021 21:40:28 +0300 Subject: [PATCH 28/49] move splitAsyncIterator to utils package --- packages/batch-execute/src/splitResult.ts | 3 +-- packages/utils/src/index.ts | 1 + packages/{batch-execute => utils}/src/splitAsyncIterator.ts | 0 .../{batch-execute => utils}/tests/splitAsyncIterator.spec.ts | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename packages/{batch-execute => utils}/src/splitAsyncIterator.ts (100%) rename packages/{batch-execute => utils}/tests/splitAsyncIterator.spec.ts (100%) diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index 5975bb715c1..1b5bc9a8111 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -2,12 +2,11 @@ import { ExecutionResult, GraphQLError } from 'graphql'; -import { AsyncExecutionResult, ExecutionPatchResult, isAsyncIterable, relocatedError } from '@graphql-tools/utils'; +import { AsyncExecutionResult, ExecutionPatchResult, isAsyncIterable, relocatedError, splitAsyncIterator } from '@graphql-tools/utils'; import { ValueOrPromise } from 'value-or-promise'; import { parseKey } from './prefix'; -import { splitAsyncIterator } from './splitAsyncIterator'; export function splitResult( mergedResult: diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 531c3a96ea9..d6e95e4a6ea 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -39,6 +39,7 @@ export * from './renameType'; export * from './collectFields'; export * from './transformInputValue'; export * from './mapAsyncIterator'; +export * from './splitAsyncIterator'; export * from './updateArgument'; export * from './implementsAbstractType'; export * from './errors'; diff --git a/packages/batch-execute/src/splitAsyncIterator.ts b/packages/utils/src/splitAsyncIterator.ts similarity index 100% rename from packages/batch-execute/src/splitAsyncIterator.ts rename to packages/utils/src/splitAsyncIterator.ts diff --git a/packages/batch-execute/tests/splitAsyncIterator.spec.ts b/packages/utils/tests/splitAsyncIterator.spec.ts similarity index 100% rename from packages/batch-execute/tests/splitAsyncIterator.spec.ts rename to packages/utils/tests/splitAsyncIterator.spec.ts From 5dd17dd93f3f4971de69a34467ee0e59b01310bb Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 16 May 2021 12:56:53 +0300 Subject: [PATCH 29/49] fix rebase --- packages/delegate/src/delegateToSchema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index efdfd7f04ff..958dfaebcf6 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -26,7 +26,6 @@ import { Subscriber, isAsyncIterable, mapAsyncIterator, - Subscriber, } from '@graphql-tools/utils'; import { From 83103bc0988029d1f5fcb15df5394b2290c97a49 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 16 May 2021 16:44:27 +0300 Subject: [PATCH 30/49] further refactor --- packages/delegate/src/delegateToSchema.ts | 173 ++++++++++++---------- 1 file changed, 98 insertions(+), 75 deletions(-) diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 958dfaebcf6..b30ffe75b98 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -23,12 +23,14 @@ import { ExecutionParams, ExecutionResult, Executor, + Request, Subscriber, isAsyncIterable, mapAsyncIterator, } from '@graphql-tools/utils'; import { + DelegationBinding, DelegationContext, IDelegateToSchemaOptions, IDelegateRequestOptions, @@ -42,6 +44,7 @@ import { Transformer } from './Transformer'; import { memoize2 } from './memoize'; import { InitialReceiver } from './InitialReceiver'; import { externalValueFromResult } from './externalValues'; +import { defaultDelegationBinding } from './delegationBindings'; export function delegateToSchema, TArgs = any>( options: IDelegateToSchemaOptions @@ -94,72 +97,13 @@ function getDelegationReturnType( export function delegateRequest, TArgs = any>(options: IDelegateRequestOptions) { const delegationContext = getDelegationContext(options); - const transformer = new Transformer(delegationContext, options.binding); - - const processedRequest = transformer.transformRequest(options.request); - - if (!options.skipValidation) { - validateRequest(delegationContext, processedRequest.document); - } - - const { operation, context, info } = delegationContext; + const operation = delegationContext.operation; if (operation === 'query' || operation === 'mutation') { - const executor = getExecutor(delegationContext); - - return new ValueOrPromise(() => executor({ - ...processedRequest, - context, - info - })).then( - executionResult => handleExecutionResult( - executionResult, - delegationContext, - originalResult => transformer.transformResult(originalResult) - ) - ).resolve(); - } - - const subscriber = getSubscriber(delegationContext); - - return subscriber({ - ...processedRequest, - context, - info, - }).then(subscriptionResult => - handleSubscriptionResult(subscriptionResult, delegationContext, originalResult => - transformer.transformResult(originalResult) - ) - ); -} - -function handleExecutionResult( - executionResult: ExecutionResult | AsyncIterableIterator, - delegationContext: DelegationContext, - resultTransformer: (originalResult: ExecutionResult) => ExecutionResult -): any { - if (isAsyncIterable(executionResult)) { - const receiver = new InitialReceiver(executionResult, delegationContext, resultTransformer); - - return receiver.getInitialResult(); + return delegateQueryOrMutation(options.request, delegationContext, options.skipValidation, options.binding); } - return externalValueFromResult(resultTransformer(executionResult), delegationContext); -} - -function handleSubscriptionResult( - subscriptionResult: AsyncIterableIterator | ExecutionResult, - delegationContext: DelegationContext, - resultTransformer: (originalResult: ExecutionResult) => any -): ExecutionResult | AsyncIterableIterator { - if (isAsyncIterable(subscriptionResult)) { - // "subscribe" to the subscription result and map the result through the transforms - return mapAsyncIterator(subscriptionResult, originalResult => ({ - [delegationContext.fieldName]: externalValueFromResult(resultTransformer(originalResult), delegationContext), - })); - } - - return resultTransformer(subscriptionResult); + return delegateSubscription(options.request, delegationContext, options.skipValidation, options.binding); } const emptyObject = {}; @@ -250,6 +194,17 @@ function validateRequest(delegationContext: DelegationContext, document: Documen } } +const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValue: Record): Executor { + return (({ document, context, variables, info }: ExecutionParams) => + execute({ + schema, + document, + contextValue: context, + variableValues: variables, + rootValue: rootValue ?? info?.rootValue, + })) as Executor; +}); + function getExecutor(delegationContext: DelegationContext): Executor { const { subschemaConfig, targetSchema, context, rootValue } = delegationContext; @@ -270,21 +225,45 @@ function getExecutor(delegationContext: DelegationContext): Executor { return executor; } -function getSubscriber(delegationContext: DelegationContext): Subscriber { - const { subschemaConfig, targetSchema, rootValue } = delegationContext; - return subschemaConfig?.subscriber || createDefaultSubscriber(targetSchema, subschemaConfig?.rootValue || rootValue); +function handleExecutionResult( + executionResult: ExecutionResult | AsyncIterableIterator, + delegationContext: DelegationContext, + resultTransformer: (originalResult: ExecutionResult) => ExecutionResult +): any { + if (isAsyncIterable(executionResult)) { + const receiver = new InitialReceiver(executionResult, delegationContext, resultTransformer); + + return receiver.getInitialResult(); + } + + return externalValueFromResult(resultTransformer(executionResult), delegationContext); } -const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValue: Record): Executor { - return (({ document, context, variables, info }: ExecutionParams) => - execute({ - schema, - document, - contextValue: context, - variableValues: variables, - rootValue: rootValue ?? info?.rootValue, - })) as Executor; -}); +export function delegateQueryOrMutation(request: Request, delegationContext: DelegationContext, skipValidation?: boolean, binding: DelegationBinding = defaultDelegationBinding) { + const transformer = new Transformer(delegationContext, binding); + + const processedRequest = transformer.transformRequest(request); + + if (!skipValidation) { + validateRequest(delegationContext, processedRequest.document); + } + + const { context, info } = delegationContext; + + const executor = getExecutor(delegationContext); + + return new ValueOrPromise(() => executor({ + ...processedRequest, + context, + info + })).then( + executionResult => handleExecutionResult( + executionResult, + delegationContext, + originalResult => transformer.transformResult(originalResult) + ) + ).resolve(); +} function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record): Subscriber { return (async ({ document, context, variables, info }: ExecutionParams) => @@ -296,3 +275,47 @@ function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record | ExecutionResult, + delegationContext: DelegationContext, + resultTransformer: (originalResult: ExecutionResult) => any +): ExecutionResult | AsyncIterableIterator { + if (isAsyncIterable(subscriptionResult)) { + // "subscribe" to the subscription result and map the result through the transforms + return mapAsyncIterator(subscriptionResult, originalResult => ({ + [delegationContext.fieldName]: externalValueFromResult(resultTransformer(originalResult), delegationContext), + })); + } + + return resultTransformer(subscriptionResult); +} + +export function delegateSubscription(request: Request, delegationContext: DelegationContext, skipValidation = false, binding = defaultDelegationBinding) { + const transformer = new Transformer(delegationContext, binding); + + const processedRequest = transformer.transformRequest(request); + + if (!skipValidation) { + validateRequest(delegationContext, processedRequest.document); + } + + const { context, info } = delegationContext; + + const subscriber = getSubscriber(delegationContext); + + return subscriber({ + ...processedRequest, + context, + info, + }).then(subscriptionResult => + handleSubscriptionResult(subscriptionResult, delegationContext, originalResult => + transformer.transformResult(originalResult) + ) + ); +} From ec4440f2b73f43e5bbbea5f96df5cd0abb4f26e9 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 18 May 2021 20:49:48 +0300 Subject: [PATCH 31/49] rename info => parentInfo for consistency --- packages/delegate/src/getMergedParent.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/delegate/src/getMergedParent.ts b/packages/delegate/src/getMergedParent.ts index 2ac06d1485b..30e422e35b6 100644 --- a/packages/delegate/src/getMergedParent.ts +++ b/packages/delegate/src/getMergedParent.ts @@ -133,7 +133,7 @@ function getMergedParentsFromFieldNodes( sourceSubschemaOrSourceSubschemas: Subschema | Array, targetSubschemas: Array, context: Record, - info: GraphQLResolveInfo + parentInfo: GraphQLResolveInfo ): Record> { if (!fieldNodes.length) { return Object.create(null); @@ -163,7 +163,7 @@ function getMergedParentsFromFieldNodes( delegationMap.forEach((fieldNodes: Array, s: Subschema) => { const resolver = mergedTypeInfo.resolvers.get(s); const selectionSet = { kind: Kind.SELECTION_SET, selections: fieldNodes }; - let maybePromise = resolver(object, context, info, s, selectionSet); + let maybePromise = resolver(object, context, parentInfo, s, selectionSet); if (isPromise(maybePromise)) { maybePromise = maybePromise.then(undefined, error => error); } @@ -171,8 +171,8 @@ function getMergedParentsFromFieldNodes( const promise = Promise.resolve(maybePromise).then(result => mergeExternalObjects( - info.schema, - responsePathAsArray(info.path), + parentInfo.schema, + responsePathAsArray(parentInfo.path), object.__typename, object, [result], @@ -190,8 +190,8 @@ function getMergedParentsFromFieldNodes( .then(results => getMergedParentsFromFieldNodes( mergedTypeInfo, mergeExternalObjects( - info.schema, - responsePathAsArray(info.path), + parentInfo.schema, + responsePathAsArray(parentInfo.path), object.__typename, object, results, @@ -201,7 +201,7 @@ function getMergedParentsFromFieldNodes( combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), nonProxiableSubschemas, context, - info + parentInfo ) ); From e9e11d9a0cbd326a2be52830c7cd1b0e3846787b Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 20 May 2021 22:39:53 +0300 Subject: [PATCH 32/49] change Receiver to emit graphql results instead of external objects WIP, now emits results with merged errors, plan to emit plain results goal: create receivers that can be more easily modified for batching --- packages/delegate/src/InitialReceiver.ts | 194 +++++++++++------- .../delegate/src/defaultMergedResolver.ts | 24 ++- packages/delegate/src/delegateToSchema.ts | 6 +- packages/delegate/src/expectantStore.ts | 4 +- packages/delegate/src/externalObjects.ts | 9 +- packages/delegate/src/externalValues.ts | 4 +- packages/delegate/src/resolveExternalValue.ts | 6 +- packages/delegate/src/types.ts | 9 +- 8 files changed, 156 insertions(+), 100 deletions(-) diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index 4b9c5dbefe6..a44b97b000a 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -3,9 +3,12 @@ import { ExecutionResult, getNamedType, GraphQLList, + GraphQLObjectType, GraphQLOutputType, GraphQLResolveInfo, + GraphQLSchema, isCompositeType, + isObjectType, Kind, responsePathAsArray, SelectionSetNode, @@ -15,12 +18,10 @@ import DataLoader from 'dataloader'; import { Repeater, Stop } from '@repeaterjs/repeater'; -import { AsyncExecutionResult, getResponseKeyFromInfo } from '@graphql-tools/utils'; +import { AsyncExecutionResult, collectFields, getResponseKeyFromInfo, GraphQLExecutionContext } from '@graphql-tools/utils'; -import { DelegationContext, ExternalObject, Receiver } from './types'; -import { getReceiver, getSubschema, getUnpathedErrors, mergeExternalObjects } from './externalObjects'; -import { resolveExternalValue } from './resolveExternalValue'; -import { externalValueFromResult, externalValueFromPatchResult } from './externalValues'; +import { DelegationContext, MergedExecutionResult, Receiver } from './types'; +import { mergeDataAndErrors } from './externalValues'; import { ExpectantStore } from './expectantStore'; import { fieldShouldStream } from './fieldShouldStream'; @@ -28,13 +29,12 @@ export class InitialReceiver implements Receiver { private readonly asyncIterable: AsyncIterable; private readonly delegationContext: DelegationContext; private readonly fieldName: string; - private readonly context: Record; private readonly asyncSelectionSets: Record; private readonly resultTransformer: (originalResult: ExecutionResult) => any; private readonly initialResultDepth: number; private deferredPatches: Record>; private streamedPatches: Record>>; - private cache: ExpectantStore; + private cache: ExpectantStore; private stoppers: Array; private loaders: Record>; private infos: Record>; @@ -47,10 +47,9 @@ export class InitialReceiver implements Receiver { this.asyncIterable = asyncIterable; this.delegationContext = delegationContext; - const { fieldName, context, info, asyncSelectionSets } = delegationContext; + const { fieldName, info, asyncSelectionSets } = delegationContext; this.fieldName = fieldName; - this.context = context; this.asyncSelectionSets = asyncSelectionSets; this.resultTransformer = resultTransformer; @@ -64,44 +63,43 @@ export class InitialReceiver implements Receiver { this.infos = Object.create(null); } - public async getInitialResult(): Promise { - let initialResult: any; + public async getInitialResult(): Promise { + const { fieldName, info, onLocatedError } = this.delegationContext; + + let initialResult: ExecutionResult; + let initialData: any; for await (const payload of this.asyncIterable) { - initialResult = externalValueFromResult(this.resultTransformer(payload), this.delegationContext, this); - if (initialResult != null) { + initialResult = this.resultTransformer(payload); + initialData = initialResult?.data?.[fieldName]; + if (initialData != null) { break; } } - this.cache.set(getResponseKeyFromInfo(this.delegationContext.info), initialResult); + + const fullPath = responsePathAsArray(info.path); + const newResult = mergeDataAndErrors(initialData, initialResult.errors, fullPath, onLocatedError); + this.cache.set(getResponseKeyFromInfo(info), newResult); this._iterate(); - return initialResult; + return newResult; } - public update(parent: ExternalObject, info: GraphQLResolveInfo): any { + public update(info: GraphQLResolveInfo, result: MergedExecutionResult): void { const path = responsePathAsArray(info.path).slice(this.initialResultDepth); const pathKey = path.join('.'); - const responseKey = path.slice().pop() as string; - const data = parent[responseKey]; - return this._update(parent, info, pathKey, responseKey, data); + this._update(info, result, pathKey); } private _update( - parent: ExternalObject, info: GraphQLResolveInfo, + result: MergedExecutionResult, pathKey: string, - responseKey: string, - data: any, - ): any { - const unpathedErrors = getUnpathedErrors(parent); - const subschema = getSubschema(parent, responseKey); - const receiver = getReceiver(parent, subschema); - const newExternalValue = resolveExternalValue(data, unpathedErrors, subschema, this.context, info, receiver); - this.onNewExternalValue( + ): void { + this.onNewResult( pathKey, - newExternalValue, + result, isCompositeType(getNamedType(info.returnType)) ? { kind: Kind.SELECTION_SET, @@ -109,10 +107,9 @@ export class InitialReceiver implements Receiver { } : undefined ); - return newExternalValue; } - public request(info: GraphQLResolveInfo): Promise { + public request(info: GraphQLResolveInfo): Promise { const path = responsePathAsArray(info.path).slice(this.initialResultDepth); const pathKey = path.join('.'); let loader = this.loaders[pathKey]; @@ -154,22 +151,25 @@ export class InitialReceiver implements Receiver { throw new Error(`Parent with key "${parentKey}" not available.`) } - const data = parent[responseKey]; + const data = parent.data[responseKey]; if (data !== undefined) { - this._update(parent, combinedInfo, pathKey, responseKey, data); + const newResult = { data, unpathedErrors: parent.unpathedErrors }; + this._update(combinedInfo, newResult, pathKey); } if (fieldShouldStream(combinedInfo)) { return infos.map(() => this._stream(pathKey)); } - const externalValue = await this.cache.request(pathKey); - return new Array(infos.length).fill(externalValue); + const result = await this.cache.request(pathKey); + return new Array(infos.length).fill(result); } - private _stream(pathKey: string): AsyncIterator { + private _stream(pathKey: string): AsyncIterator { + const cache = this.cache; return new Repeater(async (push, stop) => { - const initialValues = ((await this.cache.request(pathKey)) as unknown) as Array; + const initialResult = await cache.request(pathKey); + const initialData = initialResult.data; let stopped = false; stop.then(() => (stopped = true)); @@ -178,12 +178,13 @@ export class InitialReceiver implements Receiver { let index = 0; /* eslint-disable no-unmodified-loop-condition */ - while (!stopped && index < initialValues.length) { - await push(initialValues[index++]); + while (!stopped && index < initialData.length) { + const data = initialData[index++]; + await push({ data, unpathedErrors: initialResult.unpathedErrors }); } while (!stopped) { - await push(this.cache.request(`${pathKey}.${index++}`)); + await push(cache.request(`${pathKey}.${index++}`)); } /* eslint-disable no-unmodified-loop-condition */ }); @@ -213,14 +214,13 @@ export class InitialReceiver implements Receiver { const transformedResult = this.resultTransformer(asyncResult); if (path.length === 1) { - const newExternalValue = externalValueFromPatchResult( - transformedResult, - this.delegationContext, - this.delegationContext.info, - this - ); const pathKey = path.join('.'); - this.onNewExternalValue(pathKey, newExternalValue, this.asyncSelectionSets[asyncResult.label]); + + const { info, onLocatedError } = this.delegationContext; + const fullPath = responsePathAsArray(info.path); + const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, fullPath, onLocatedError); + + this.onNewResult(pathKey, newResult, this.asyncSelectionSets[asyncResult.label]); continue; } @@ -250,8 +250,11 @@ export class InitialReceiver implements Receiver { continue; } - const newExternalValue = externalValueFromPatchResult(transformedResult, this.delegationContext, info, this); - this.onNewExternalValue(`${pathKey}.${index}`, newExternalValue, this.asyncSelectionSets[asyncResult.label]); + const { onLocatedError } = this.delegationContext; + const fullPath = responsePathAsArray(info.path); + const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, fullPath, onLocatedError); + + this.onNewResult(`${pathKey}.${index}`, newResult, this.asyncSelectionSets[asyncResult.label]); continue; } @@ -271,8 +274,11 @@ export class InitialReceiver implements Receiver { continue; } - const newExternalValue = externalValueFromPatchResult(transformedResult, this.delegationContext, info, this); - this.onNewExternalValue(`${pathKey}`, newExternalValue, this.asyncSelectionSets[asyncResult.label]); + const { onLocatedError } = this.delegationContext; + const fullPath = responsePathAsArray(info.path); + const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, fullPath, onLocatedError); + + this.onNewResult(pathKey, newResult, this.asyncSelectionSets[asyncResult.label]); } setTimeout(() => { @@ -281,36 +287,32 @@ export class InitialReceiver implements Receiver { }); } - private onNewExternalValue(pathKey: string, newExternalValue: any, selectionSet: SelectionSetNode): void { - const externalValue = this.cache.get(pathKey); - this.cache.set( - pathKey, - externalValue === undefined - ? newExternalValue - : mergeExternalObjects( - this.delegationContext.info.schema, - pathKey.split('.'), - externalValue.__typename, - externalValue, - [newExternalValue], - [selectionSet] - ) - ); + private onNewResult(pathKey: string, newResult: MergedExecutionResult, selectionSet: SelectionSetNode): void { + const result = this.cache.get(pathKey); + const mergedResult = result === undefined + ? newResult + : mergeResults( + this.delegationContext.info.schema, + result.data.__typename, + result, + newResult, + selectionSet + ); + + this.cache.set(pathKey, mergedResult); const infosByParentKey = this.infos[pathKey]; if (infosByParentKey !== undefined) { - const unpathedErrors = getUnpathedErrors(newExternalValue); + const unpathedErrors = newResult.unpathedErrors; Object.keys(infosByParentKey).forEach(responseKey => { const info = infosByParentKey[responseKey]; - const data = newExternalValue[responseKey]; + const data = newResult.data[responseKey]; if (data !== undefined) { - const subschema = getSubschema(newExternalValue, responseKey); - const receiver = getReceiver(newExternalValue, subschema); - const subExternalValue = resolveExternalValue(data, unpathedErrors, subschema, this.context, info, receiver); + const subResult = { data, unpathedErrors }; const subPathKey = `${pathKey}.${responseKey}`; - this.onNewExternalValue( + this.onNewResult( subPathKey, - subExternalValue, + subResult, isCompositeType(getNamedType(info.returnType)) ? { kind: Kind.SELECTION_SET, @@ -321,16 +323,16 @@ export class InitialReceiver implements Receiver { } }); } - - this.cache.set(pathKey, newExternalValue); } private onNewInfo(pathKey: string, info: GraphQLResolveInfo): void { const deferredPatches = this.deferredPatches[pathKey]; if (deferredPatches !== undefined) { deferredPatches.forEach(deferredPatch => { - const newExternalValue = externalValueFromPatchResult(deferredPatch, this.delegationContext, info, this); - this.onNewExternalValue(pathKey, newExternalValue, this.asyncSelectionSets[deferredPatch.label]); + const { onLocatedError } = this.delegationContext; + const fullPath = responsePathAsArray(info.path); + const newResult = mergeDataAndErrors(deferredPatch.data, deferredPatch.errors, fullPath, onLocatedError); + this.onNewResult(pathKey, newResult, this.asyncSelectionSets[deferredPatch.label]); }); } @@ -342,10 +344,44 @@ export class InitialReceiver implements Receiver { }; Object.entries(streamedPatches).forEach(([index, indexPatches]) => { indexPatches.forEach(patch => { - const newExternalValue = externalValueFromPatchResult(patch, this.delegationContext, listMemberInfo, this); - this.onNewExternalValue(`${pathKey}.${index}`, newExternalValue, this.asyncSelectionSets[patch.label]); + const { onLocatedError } = this.delegationContext; + const fullPath = responsePathAsArray(listMemberInfo.path); + const newResult = mergeDataAndErrors(patch.data, patch.errors, fullPath, onLocatedError); + this.onNewResult(`${pathKey}.${index}`, newResult, this.asyncSelectionSets[patch.label]); }); }); } } } + +export function mergeResults( + schema: GraphQLSchema, + typeName: string, + target: MergedExecutionResult, + source: MergedExecutionResult, + selectionSet: SelectionSetNode +): MergedExecutionResult { + if (isObjectType(schema.getType(typeName))) { + const fieldNodes = collectFields( + { + schema, + variableValues: {}, + fragments: {}, + } as GraphQLExecutionContext, + schema.getType(typeName) as GraphQLObjectType, + selectionSet, + Object.create(null), + Object.create(null) + ); + + const targetData = target.data; + const sourceData = source.data; + Object.keys(fieldNodes).forEach(responseKey => { + targetData[responseKey] = sourceData[responseKey]; + }); + } + + target.unpathedErrors.push(...source.unpathedErrors ?? []); + + return target; +} diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index 0b68c9c9dc1..2c630e7326a 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -1,8 +1,8 @@ -import { GraphQLResolveInfo, defaultFieldResolver } from 'graphql'; +import { GraphQLResolveInfo, defaultFieldResolver, GraphQLList, GraphQLOutputType } from 'graphql'; -import { getResponseKeyFromInfo } from '@graphql-tools/utils'; +import { getResponseKeyFromInfo, mapAsyncIterator } from '@graphql-tools/utils'; -import { ExternalObject } from './types'; +import { ExternalObject, MergedExecutionResult } from './types'; import { resolveExternalValue } from './resolveExternalValue'; import { @@ -68,14 +68,24 @@ function resolveField( const data = parent[responseKey]; if (receiver !== undefined) { if (fieldShouldStream(info)) { - return receiver.request(info); + return receiver.request(info).then(asyncIterator => { + const listMemberInfo: GraphQLResolveInfo = { + ...info, + returnType: (info.returnType as GraphQLList).ofType, + }; + return mapAsyncIterator(asyncIterator as AsyncIterableIterator, ({ data, unpathedErrors }) => + resolveExternalValue(data, unpathedErrors, fieldSubschema, context, listMemberInfo, receiver)); + }); } - if (data !== undefined) { - return receiver.update(parent, info); + if (data === undefined) { + return receiver.request(info).then(result => + resolveExternalValue((result as MergedExecutionResult).data, (result as MergedExecutionResult).unpathedErrors, fieldSubschema, context, info, receiver)); } - return receiver.request(info); + const unpathedErrors = getUnpathedErrors(parent); + receiver.update(info, { data, unpathedErrors }); + return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info, receiver); } if (data !== undefined) { diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index b30ffe75b98..da94465f18e 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -45,6 +45,7 @@ import { memoize2 } from './memoize'; import { InitialReceiver } from './InitialReceiver'; import { externalValueFromResult } from './externalValues'; import { defaultDelegationBinding } from './delegationBindings'; +import { resolveExternalValue } from './resolveExternalValue'; export function delegateToSchema, TArgs = any>( options: IDelegateToSchemaOptions @@ -233,7 +234,10 @@ function handleExecutionResult( if (isAsyncIterable(executionResult)) { const receiver = new InitialReceiver(executionResult, delegationContext, resultTransformer); - return receiver.getInitialResult(); + return receiver.getInitialResult().then(({ data, unpathedErrors}) => { + const { subschema, context, info, returnType } = delegationContext; + return resolveExternalValue(data, unpathedErrors, subschema, context, info, receiver, returnType); + }); } return externalValueFromResult(resultTransformer(executionResult), delegationContext); diff --git a/packages/delegate/src/expectantStore.ts b/packages/delegate/src/expectantStore.ts index 109b29c2e12..0a199e1c156 100644 --- a/packages/delegate/src/expectantStore.ts +++ b/packages/delegate/src/expectantStore.ts @@ -27,11 +27,11 @@ export class ExpectantStore { return this.cache[key]; } - request(key: string): Promise | T { + request(key: string): Promise { const value = this.cache[key]; if (value !== undefined) { - return value; + return Promise.resolve(value); } let settlers = this.settlers[key]; diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts index b0cbb5452ae..f39f217a079 100644 --- a/packages/delegate/src/externalObjects.ts +++ b/packages/delegate/src/externalObjects.ts @@ -26,7 +26,7 @@ export function isExternalObject(data: any): data is ExternalObject { return data != null && data[UNPATHED_ERRORS_SYMBOL] !== undefined; } -export function annotateExternalObject( +export function createExternalObject( object: any, errors: Array, subschema: GraphQLSchema | SubschemaConfig, @@ -42,7 +42,9 @@ export function annotateExternalObject( const receiverMap: Map = new Map(); receiverMap.set(subschema, receiver); - Object.defineProperties(object, { + const newObject = { ...object }; + + Object.defineProperties(newObject, { [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, [INITIAL_POSSIBLE_FIELDS]: { value: initialPossibleFields }, [INFO_SYMBOL]: { value: info }, @@ -50,7 +52,8 @@ export function annotateExternalObject( [UNPATHED_ERRORS_SYMBOL]: { value: errors }, [RECEIVER_MAP_SYMBOL]: { value: receiverMap }, }); - return object; + + return newObject; } export function getSubschema(object: ExternalObject, responseKey?: string): GraphQLSchema | SubschemaConfig { diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index 18880d60116..eade666e017 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -58,8 +58,8 @@ function externalValueFromDataAndErrors( export function mergeDataAndErrors( data: any, - errors: ReadonlyArray, - path: Array, + errors: ReadonlyArray = [], + path: ReadonlyArray, onLocatedError: (originalError: GraphQLError) => GraphQLError, index = 1 ): { data: any; unpathedErrors: Array } { diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 3b1c4c6037a..8ea3f2d241e 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -14,7 +14,7 @@ import { import AggregateError from '@ardatan/aggregate-error'; import { Receiver, SubschemaConfig } from './types'; -import { annotateExternalObject, isExternalObject } from './externalObjects'; +import { createExternalObject, isExternalObject } from './externalObjects'; export function resolveExternalValue( result: any, @@ -57,9 +57,7 @@ function resolveExternalObject( return object; } - annotateExternalObject(object, unpathedErrors, subschema, info, receiver); - - return object; + return createExternalObject(object, unpathedErrors, subschema, info, receiver); } function resolveExternalList( diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index ba0710e6047..8bff7ecef2d 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -200,9 +200,14 @@ export interface StitchingInfo> { mergedTypes: Record>; } +export interface MergedExecutionResult> { + unpathedErrors: Array; + data: TData; +} + export interface Receiver { - request: (info: GraphQLResolveInfo) => Promise; - update: (parent: ExternalObject, info: GraphQLResolveInfo) => any; + request: (info: GraphQLResolveInfo) => Promise>; + update: (info: GraphQLResolveInfo, result: MergedExecutionResult) => void; } export interface ExternalObject> { From 1faddee6a692d2dcd94562f4ffbceea2635047ef Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 23 May 2021 21:27:59 +0300 Subject: [PATCH 33/49] remove unused externalValueFromPatchResult --- packages/delegate/src/externalValues.ts | 40 ++++--------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index eade666e017..1bcf986fd07 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -1,8 +1,8 @@ -import { GraphQLError, responsePathAsArray, locatedError, GraphQLResolveInfo } from 'graphql'; +import { GraphQLError, responsePathAsArray, locatedError } from 'graphql'; import AggregateError from '@ardatan/aggregate-error'; -import { ExecutionPatchResult, ExecutionResult, relocatedError } from '@graphql-tools/utils'; +import { ExecutionResult, relocatedError } from '@graphql-tools/utils'; import { DelegationContext, Receiver } from './types'; import { resolveExternalValue } from './resolveExternalValue'; @@ -12,39 +12,10 @@ export function externalValueFromResult( delegationContext: DelegationContext, receiver?: Receiver ): any { - return externalValueFromDataAndErrors( - originalResult.data?.[delegationContext.fieldName], - originalResult.errors ?? [], - delegationContext, - receiver - ); -} - -export function externalValueFromPatchResult( - originalResult: ExecutionPatchResult, - delegationContext: DelegationContext, - info: GraphQLResolveInfo, - receiver: Receiver -): any { - return externalValueFromDataAndErrors( - originalResult.data, - originalResult.errors ?? [], - { - ...delegationContext, - info, - returnType: info.returnType, - }, - receiver - ); -} + const { fieldName, context, subschema, onLocatedError, returnType, info } = delegationContext; -function externalValueFromDataAndErrors( - data: any, - errors: ReadonlyArray, - delegationContext: DelegationContext, - receiver?: Receiver -): any { - const { context, subschema, onLocatedError, returnType, info } = delegationContext; + const data = originalResult.data?.[fieldName]; + const errors = originalResult.errors ?? []; const { data: newData, unpathedErrors } = mergeDataAndErrors( data, @@ -54,6 +25,7 @@ function externalValueFromDataAndErrors( ); return resolveExternalValue(newData, unpathedErrors, subschema, context, info, receiver, returnType); + } export function mergeDataAndErrors( From 2153fb0a1205e3f8f872c5811bb967242eb7ba3e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 23 May 2021 21:56:00 +0300 Subject: [PATCH 34/49] refactor => change resolveExternalValue => createExternalValue --- packages/delegate/src/InitialReceiver.ts | 2 +- .../delegate/src/defaultMergedResolver.ts | 22 +-- packages/delegate/src/delegateToSchema.ts | 5 +- packages/delegate/src/externalValues.ts | 167 ++++++++++++------ packages/delegate/src/index.ts | 2 +- packages/delegate/src/mergeDataAndErrors.ts | 67 +++++++ packages/delegate/src/resolveExternalValue.ts | 134 -------------- .../wrap/src/generateProxyingResolvers.ts | 4 +- 8 files changed, 196 insertions(+), 207 deletions(-) create mode 100644 packages/delegate/src/mergeDataAndErrors.ts delete mode 100644 packages/delegate/src/resolveExternalValue.ts diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index a44b97b000a..76e47d69ddb 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -21,7 +21,7 @@ import { Repeater, Stop } from '@repeaterjs/repeater'; import { AsyncExecutionResult, collectFields, getResponseKeyFromInfo, GraphQLExecutionContext } from '@graphql-tools/utils'; import { DelegationContext, MergedExecutionResult, Receiver } from './types'; -import { mergeDataAndErrors } from './externalValues'; +import { mergeDataAndErrors } from './mergeDataAndErrors'; import { ExpectantStore } from './expectantStore'; import { fieldShouldStream } from './fieldShouldStream'; diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index 2c630e7326a..ffee9a28e81 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -4,7 +4,7 @@ import { getResponseKeyFromInfo, mapAsyncIterator } from '@graphql-tools/utils'; import { ExternalObject, MergedExecutionResult } from './types'; -import { resolveExternalValue } from './resolveExternalValue'; +import { createExternalValue } from './externalValues'; import { getInitialPossibleFields, getReceiver, @@ -44,8 +44,8 @@ export function defaultMergedResolver( const data = parent[responseKey]; if (data !== undefined) { const unpathedErrors = getUnpathedErrors(parent); - const fieldSubschema = getSubschema(parent, responseKey); - return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info); + const subschema = getSubschema(parent, responseKey); + return createExternalValue(data, unpathedErrors, subschema, context, info); } } else if (info.fieldNodes[0].name.value in initialPossibleFields) { return resolveField(parent, responseKey, context, info); @@ -62,8 +62,8 @@ function resolveField( context: Record, info: GraphQLResolveInfo ): any { - const fieldSubschema = getSubschema(parent, responseKey); - const receiver = getReceiver(parent, fieldSubschema); + const subschema = getSubschema(parent, responseKey); + const receiver = getReceiver(parent, subschema); const data = parent[responseKey]; if (receiver !== undefined) { @@ -74,23 +74,25 @@ function resolveField( returnType: (info.returnType as GraphQLList).ofType, }; return mapAsyncIterator(asyncIterator as AsyncIterableIterator, ({ data, unpathedErrors }) => - resolveExternalValue(data, unpathedErrors, fieldSubschema, context, listMemberInfo, receiver)); + createExternalValue(data, unpathedErrors, subschema, context, listMemberInfo, receiver)); }); } if (data === undefined) { - return receiver.request(info).then(result => - resolveExternalValue((result as MergedExecutionResult).data, (result as MergedExecutionResult).unpathedErrors, fieldSubschema, context, info, receiver)); + return receiver.request(info).then(result => { + const { data, unpathedErrors } = result as MergedExecutionResult; + return createExternalValue(data, unpathedErrors, subschema, context, info, receiver); + }); } const unpathedErrors = getUnpathedErrors(parent); receiver.update(info, { data, unpathedErrors }); - return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info, receiver); + return createExternalValue(data, unpathedErrors, subschema, context, info, receiver); } if (data !== undefined) { const unpathedErrors = getUnpathedErrors(parent); - return resolveExternalValue(data, unpathedErrors, fieldSubschema, context, info, receiver); + return createExternalValue(data, unpathedErrors, subschema, context, info, receiver); } // throw error? diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index da94465f18e..e3c93bc6a8b 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -43,9 +43,8 @@ import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; import { Transformer } from './Transformer'; import { memoize2 } from './memoize'; import { InitialReceiver } from './InitialReceiver'; -import { externalValueFromResult } from './externalValues'; +import { createExternalValue, externalValueFromResult } from './externalValues'; import { defaultDelegationBinding } from './delegationBindings'; -import { resolveExternalValue } from './resolveExternalValue'; export function delegateToSchema, TArgs = any>( options: IDelegateToSchemaOptions @@ -236,7 +235,7 @@ function handleExecutionResult( return receiver.getInitialResult().then(({ data, unpathedErrors}) => { const { subschema, context, info, returnType } = delegationContext; - return resolveExternalValue(data, unpathedErrors, subschema, context, info, receiver, returnType); + return createExternalValue(data, unpathedErrors, subschema, context, info, receiver, returnType); }); } diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index 1bcf986fd07..c73927e9282 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -1,11 +1,25 @@ -import { GraphQLError, responsePathAsArray, locatedError } from 'graphql'; +import { + GraphQLError, + GraphQLList, + GraphQLResolveInfo, + GraphQLOutputType, + GraphQLSchema, + GraphQLType, + getNullableType, + isCompositeType, + isLeafType, + isListType, + locatedError, + responsePathAsArray, +} from 'graphql'; import AggregateError from '@ardatan/aggregate-error'; -import { ExecutionResult, relocatedError } from '@graphql-tools/utils'; +import { ExecutionResult } from '@graphql-tools/utils'; -import { DelegationContext, Receiver } from './types'; -import { resolveExternalValue } from './resolveExternalValue'; +import { DelegationContext, Receiver, SubschemaConfig } from './types'; +import { createExternalObject, isExternalObject } from './externalObjects'; +import { mergeDataAndErrors } from './mergeDataAndErrors'; export function externalValueFromResult( originalResult: ExecutionResult, @@ -24,71 +38,112 @@ export function externalValueFromResult( onLocatedError ); - return resolveExternalValue(newData, unpathedErrors, subschema, context, info, receiver, returnType); - + return createExternalValue(newData, unpathedErrors, subschema, context, info, receiver, returnType); } -export function mergeDataAndErrors( +export function createExternalValue( data: any, - errors: ReadonlyArray = [], - path: ReadonlyArray, - onLocatedError: (originalError: GraphQLError) => GraphQLError, - index = 1 -): { data: any; unpathedErrors: Array } { - if (data == null) { - if (!errors.length) { - return { data: null, unpathedErrors: [] }; - } + unpathedErrors: Array = [], + subschema: GraphQLSchema | SubschemaConfig, + context: Record, + info: GraphQLResolveInfo, + receiver?: Receiver, + returnType: GraphQLOutputType = info?.returnType +): any { + const type = getNullableType(returnType); - 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)); + if (data instanceof Error) { + return data; + } - return { data: relocatedError(errors[0], newPath), unpathedErrors: [] }; + if (data == null) { + return reportUnpathedErrorsViaNull(unpathedErrors); + } + + if (isLeafType(type)) { + return type.parseValue(data); + } else if (isCompositeType(type)) { + if (isExternalObject(data)) { + return data; } + return createExternalObject(data, unpathedErrors, subschema, info, receiver); + } else if (isListType(type)) { + return createExternalList(type, data, unpathedErrors, subschema, context, info, receiver); + } +} - const newError = locatedError(new AggregateError(errors), undefined, path); +function createExternalList( + type: GraphQLList, + list: Array, + unpathedErrors: Array, + subschema: GraphQLSchema | SubschemaConfig, + context: Record, + info: GraphQLResolveInfo, + receiver?: Receiver +) { + return list.map(listMember => + createExternalListMember( + getNullableType(type.ofType), + listMember, + unpathedErrors, + subschema, + context, + info, + receiver + ) + ); +} - return { data: newError, unpathedErrors: [] }; +function createExternalListMember( + type: GraphQLType, + listMember: any, + unpathedErrors: Array, + subschema: GraphQLSchema | SubschemaConfig, + context: Record, + info: GraphQLResolveInfo, + receiver?: Receiver +): any { + if (listMember instanceof Error) { + return listMember; } - if (!errors.length) { - return { data, unpathedErrors: [] }; + if (listMember == null) { + return reportUnpathedErrorsViaNull(unpathedErrors); } - let unpathedErrors: Array = []; - - const errorMap: Record> = Object.create(null); - errors.forEach(error => { - const pathSegment = error.path?.[index]; - if (pathSegment != null) { - const pathSegmentErrors = errorMap[pathSegment]; - if (pathSegmentErrors === undefined) { - errorMap[pathSegment] = [error]; - } else { - pathSegmentErrors.push(error); - } - } else { - unpathedErrors.push(error); + if (isLeafType(type)) { + return type.parseValue(listMember); + } else if (isCompositeType(type)) { + if (isExternalObject(listMember)) { + return listMember; } - }); - - Object.keys(errorMap).forEach(pathSegment => { - if (data[pathSegment] !== undefined) { - const { data: newData, unpathedErrors: newErrors } = mergeDataAndErrors( - data[pathSegment], - errorMap[pathSegment], - path, - onLocatedError, - index + 1 - ); - data[pathSegment] = newData; - unpathedErrors = unpathedErrors.concat(newErrors); - } else { - unpathedErrors = unpathedErrors.concat(errorMap[pathSegment]); + return createExternalObject(listMember, unpathedErrors, subschema, info, receiver); + } else if (isListType(type)) { + return createExternalList(type, listMember, unpathedErrors, subschema, context, info, receiver); + } +} + +const reportedErrors: WeakMap = new Map(); + +function reportUnpathedErrorsViaNull(unpathedErrors: Array) { + if (unpathedErrors.length) { + const unreportedErrors: Array = []; + unpathedErrors.forEach(error => { + 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); + return locatedError(combinedError, undefined, unreportedErrors[0].path); } - }); + } - return { data, unpathedErrors }; + return null; } diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 20c2153a7ea..09a507f84af 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -6,8 +6,8 @@ export * from './delegateToSchema'; export * from './delegationBindings'; export * from './externalObjects'; export * from './externalValues'; +export * from './mergeDataAndErrors'; export * from './getMergedParent'; -export * from './resolveExternalValue'; export * from './subschemaConfig'; export * from './transforms'; export * from './types'; diff --git a/packages/delegate/src/mergeDataAndErrors.ts b/packages/delegate/src/mergeDataAndErrors.ts new file mode 100644 index 00000000000..f9363ec3518 --- /dev/null +++ b/packages/delegate/src/mergeDataAndErrors.ts @@ -0,0 +1,67 @@ +import { GraphQLError, locatedError } from 'graphql'; +import { relocatedError } from '@graphql-tools/utils'; + +export function mergeDataAndErrors( + data: any, + errors: ReadonlyArray = [], + path: ReadonlyArray, + 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: [] }; + } + + const newError = locatedError(new AggregateError(errors), undefined, path); + + return { data: newError, unpathedErrors: [] }; + } + + if (!errors.length) { + return { data, unpathedErrors: [] }; + } + + let unpathedErrors: Array = []; + + const errorMap: Record> = Object.create(null); + errors.forEach(error => { + const pathSegment = error.path?.[index]; + if (pathSegment != null) { + const pathSegmentErrors = errorMap[pathSegment]; + if (pathSegmentErrors === undefined) { + errorMap[pathSegment] = [error]; + } else { + pathSegmentErrors.push(error); + } + } else { + unpathedErrors.push(error); + } + }); + + Object.keys(errorMap).forEach(pathSegment => { + if (data[pathSegment] !== undefined) { + const { data: newData, unpathedErrors: newErrors } = mergeDataAndErrors( + data[pathSegment], + errorMap[pathSegment], + path, + onLocatedError, + index + 1 + ); + data[pathSegment] = newData; + unpathedErrors = unpathedErrors.concat(newErrors); + } else { + unpathedErrors = unpathedErrors.concat(errorMap[pathSegment]); + } + }); + + return { data, unpathedErrors }; +} diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts deleted file mode 100644 index 8ea3f2d241e..00000000000 --- a/packages/delegate/src/resolveExternalValue.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - GraphQLResolveInfo, - getNullableType, - isCompositeType, - isLeafType, - isListType, - GraphQLError, - GraphQLSchema, - GraphQLList, - GraphQLType, - locatedError, -} from 'graphql'; - -import AggregateError from '@ardatan/aggregate-error'; - -import { Receiver, SubschemaConfig } from './types'; -import { createExternalObject, isExternalObject } from './externalObjects'; - -export function resolveExternalValue( - result: any, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context: Record, - info: GraphQLResolveInfo, - receiver?: Receiver, - returnType = info?.returnType -): any { - const type = getNullableType(returnType); - - if (result instanceof Error) { - return result; - } - - if (result == null) { - return reportUnpathedErrorsViaNull(unpathedErrors); - } - - if (isLeafType(type)) { - return type.parseValue(result); - } else if (isCompositeType(type)) { - return resolveExternalObject(result, unpathedErrors, subschema, info, receiver); - } else if (isListType(type)) { - return resolveExternalList(type, result, unpathedErrors, subschema, context, info, receiver); - } -} - -function resolveExternalObject( - object: any, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - info: GraphQLResolveInfo, - receiver?: Receiver -) { - // 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)) { - return object; - } - - return createExternalObject(object, unpathedErrors, subschema, info, receiver); -} - -function resolveExternalList( - type: GraphQLList, - list: Array, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context: Record, - info: GraphQLResolveInfo, - receiver?: Receiver -) { - return list.map(listMember => - resolveExternalListMember( - getNullableType(type.ofType), - listMember, - unpathedErrors, - subschema, - context, - info, - receiver - ) - ); -} - -function resolveExternalListMember( - type: GraphQLType, - listMember: any, - unpathedErrors: Array, - subschema: GraphQLSchema | SubschemaConfig, - context: Record, - info: GraphQLResolveInfo, - receiver?: Receiver -): any { - if (listMember instanceof Error) { - return listMember; - } - - if (listMember == null) { - return reportUnpathedErrorsViaNull(unpathedErrors); - } - - if (isLeafType(type)) { - return type.parseValue(listMember); - } else if (isCompositeType(type)) { - return resolveExternalObject(listMember, unpathedErrors, subschema, info, receiver); - } else if (isListType(type)) { - return resolveExternalList(type, listMember, unpathedErrors, subschema, context, info, receiver); - } -} - -const reportedErrors: WeakMap = new Map(); - -function reportUnpathedErrorsViaNull(unpathedErrors: Array) { - if (unpathedErrors.length) { - const unreportedErrors: Array = []; - unpathedErrors.forEach(error => { - 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); - return locatedError(combinedError, undefined, unreportedErrors[0].path); - } - } - - return null; -} diff --git a/packages/wrap/src/generateProxyingResolvers.ts b/packages/wrap/src/generateProxyingResolvers.ts index 579d1073988..008726d4294 100644 --- a/packages/wrap/src/generateProxyingResolvers.ts +++ b/packages/wrap/src/generateProxyingResolvers.ts @@ -4,7 +4,7 @@ import { getResponseKeyFromInfo } from '@graphql-tools/utils'; import { delegateToSchema, getSubschema, - resolveExternalValue, + createExternalValue, SubschemaConfig, ICreateProxyingResolverOptions, applySchemaTransforms, @@ -84,7 +84,7 @@ function createPossiblyNestedProxyingResolver( if (subschemaConfig === subschema && parent[responseKey] !== undefined) { const unpathedErrors = getUnpathedErrors(parent); const receiver = getReceiver(parent, subschema); - return resolveExternalValue(parent[responseKey], unpathedErrors, subschema, context, info, receiver); + return createExternalValue(parent[responseKey], unpathedErrors, subschema, context, info, receiver); } } } From ec352cf4833eba15c2cdc1fd39baa456b7147ef8 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 23 May 2021 21:59:30 +0300 Subject: [PATCH 35/49] no longer necessary because createExternalObject creates a new object rather than annotating --- packages/delegate/src/externalValues.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index c73927e9282..86c5ecdb429 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -18,7 +18,7 @@ import AggregateError from '@ardatan/aggregate-error'; import { ExecutionResult } from '@graphql-tools/utils'; import { DelegationContext, Receiver, SubschemaConfig } from './types'; -import { createExternalObject, isExternalObject } from './externalObjects'; +import { createExternalObject } from './externalObjects'; import { mergeDataAndErrors } from './mergeDataAndErrors'; export function externalValueFromResult( @@ -63,9 +63,6 @@ export function createExternalValue( if (isLeafType(type)) { return type.parseValue(data); } else if (isCompositeType(type)) { - if (isExternalObject(data)) { - return data; - } return createExternalObject(data, unpathedErrors, subschema, info, receiver); } else if (isListType(type)) { return createExternalList(type, data, unpathedErrors, subschema, context, info, receiver); @@ -114,9 +111,6 @@ function createExternalListMember( if (isLeafType(type)) { return type.parseValue(listMember); } else if (isCompositeType(type)) { - if (isExternalObject(listMember)) { - return listMember; - } return createExternalObject(listMember, unpathedErrors, subschema, info, receiver); } else if (isListType(type)) { return createExternalList(type, listMember, unpathedErrors, subschema, context, info, receiver); From 5bd77cbccb5b85f86d0b46c11eb113d80b5111cc Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 24 May 2021 21:25:35 +0300 Subject: [PATCH 36/49] append base path to errors only when creating external values this removes this logic from the receiver, which will help receiver proxying --- packages/delegate/src/mergeDataAndErrors.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/delegate/src/mergeDataAndErrors.ts b/packages/delegate/src/mergeDataAndErrors.ts index f9363ec3518..8a03639f72d 100644 --- a/packages/delegate/src/mergeDataAndErrors.ts +++ b/packages/delegate/src/mergeDataAndErrors.ts @@ -4,8 +4,7 @@ import { relocatedError } from '@graphql-tools/utils'; export function mergeDataAndErrors( data: any, errors: ReadonlyArray = [], - path: ReadonlyArray, - onLocatedError: (originalError: GraphQLError) => GraphQLError, + onLocatedError = (originalError: GraphQLError) => originalError, index = 1 ): { data: any; unpathedErrors: Array } { if (data == null) { @@ -14,16 +13,19 @@ export function mergeDataAndErrors( } 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: [] }; + const error = onLocatedError(errors[0]); + const newPath = error.path === undefined ? [] : error.path.slice(1); + const newError = relocatedError(error, newPath); + return { data: newError, unpathedErrors: [] }; } - const newError = locatedError(new AggregateError(errors), undefined, path); + const newErrors = errors.map(error => onLocatedError(error)); + const firstError = newErrors[0]; + const newPath = firstError.path === undefined ? [] : firstError.path.slice(1); + + const aggregateError = locatedError(new AggregateError(newErrors), undefined, newPath); - return { data: newError, unpathedErrors: [] }; + return { data: aggregateError, unpathedErrors: [] }; } if (!errors.length) { @@ -52,7 +54,6 @@ export function mergeDataAndErrors( const { data: newData, unpathedErrors: newErrors } = mergeDataAndErrors( data[pathSegment], errorMap[pathSegment], - path, onLocatedError, index + 1 ); From e84adb888e743f1c702d6220a06ec85331c4458f Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 24 May 2021 21:30:23 +0300 Subject: [PATCH 37/49] stitch new base paths to errors only when creating leaf external values, this allows the merged results to be agnostic of stitched base paths, and enables implementing batching of merged results irrespective of possible changes to the base paths --- packages/batch-delegate/src/getLoader.ts | 3 +- packages/delegate/src/InitialReceiver.ts | 30 ++++++------------- .../delegate/src/defaultMergedResolver.ts | 13 ++++---- packages/delegate/src/delegateToSchema.ts | 7 ++++- packages/delegate/src/externalObjects.ts | 26 ++++++++++++---- packages/delegate/src/externalValues.ts | 26 +++++++++++----- packages/delegate/src/getMergedParent.ts | 7 ----- packages/delegate/src/symbols.ts | 1 + packages/delegate/src/types.ts | 2 ++ .../wrap/src/generateProxyingResolvers.ts | 16 +++++----- 10 files changed, 76 insertions(+), 55 deletions(-) diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 03d2d1b2742..03a5bb8515c 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -19,8 +19,7 @@ function createBatchFn(options: BatchDelegateOptions) { return async (keys: ReadonlyArray) => { const results = await delegateToSchema({ returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), - onLocatedError: originalError => - relocatedError(originalError, originalError.path.slice(0, 0).concat(originalError.path.slice(2))), + onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)), args: argsFromKeys(keys), ...(lazyOptionsFn == null ? options : lazyOptionsFn(options)), }); diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index 76e47d69ddb..6dfc43f92dc 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -2,9 +2,7 @@ import { ExecutionPatchResult, ExecutionResult, getNamedType, - GraphQLList, GraphQLObjectType, - GraphQLOutputType, GraphQLResolveInfo, GraphQLSchema, isCompositeType, @@ -76,8 +74,7 @@ export class InitialReceiver implements Receiver { } } - const fullPath = responsePathAsArray(info.path); - const newResult = mergeDataAndErrors(initialData, initialResult.errors, fullPath, onLocatedError); + const newResult = mergeDataAndErrors(initialData, initialResult.errors, onLocatedError); this.cache.set(getResponseKeyFromInfo(info), newResult); this._iterate(); @@ -142,7 +139,7 @@ export class InitialReceiver implements Receiver { if (infosByParentKey[responseKey] === undefined) { infosByParentKey[responseKey] = combinedInfo; - this.onNewInfo(pathKey, combinedInfo); + this.onNewInfo(pathKey); } const parent = this.cache.get(parentKey); @@ -216,9 +213,8 @@ export class InitialReceiver implements Receiver { if (path.length === 1) { const pathKey = path.join('.'); - const { info, onLocatedError } = this.delegationContext; - const fullPath = responsePathAsArray(info.path); - const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, fullPath, onLocatedError); + const { onLocatedError } = this.delegationContext; + const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); this.onNewResult(pathKey, newResult, this.asyncSelectionSets[asyncResult.label]); continue; @@ -251,8 +247,7 @@ export class InitialReceiver implements Receiver { } const { onLocatedError } = this.delegationContext; - const fullPath = responsePathAsArray(info.path); - const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, fullPath, onLocatedError); + const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); this.onNewResult(`${pathKey}.${index}`, newResult, this.asyncSelectionSets[asyncResult.label]); continue; @@ -275,8 +270,7 @@ export class InitialReceiver implements Receiver { } const { onLocatedError } = this.delegationContext; - const fullPath = responsePathAsArray(info.path); - const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, fullPath, onLocatedError); + const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); this.onNewResult(pathKey, newResult, this.asyncSelectionSets[asyncResult.label]); } @@ -325,28 +319,22 @@ export class InitialReceiver implements Receiver { } } - private onNewInfo(pathKey: string, info: GraphQLResolveInfo): void { + private onNewInfo(pathKey: string): void { const deferredPatches = this.deferredPatches[pathKey]; if (deferredPatches !== undefined) { deferredPatches.forEach(deferredPatch => { const { onLocatedError } = this.delegationContext; - const fullPath = responsePathAsArray(info.path); - const newResult = mergeDataAndErrors(deferredPatch.data, deferredPatch.errors, fullPath, onLocatedError); + const newResult = mergeDataAndErrors(deferredPatch.data, deferredPatch.errors, onLocatedError); this.onNewResult(pathKey, newResult, this.asyncSelectionSets[deferredPatch.label]); }); } const streamedPatches = this.streamedPatches[pathKey]; if (streamedPatches !== undefined) { - const listMemberInfo: GraphQLResolveInfo = { - ...info, - returnType: (info.returnType as GraphQLList).ofType, - }; Object.entries(streamedPatches).forEach(([index, indexPatches]) => { indexPatches.forEach(patch => { const { onLocatedError } = this.delegationContext; - const fullPath = responsePathAsArray(listMemberInfo.path); - const newResult = mergeDataAndErrors(patch.data, patch.errors, fullPath, onLocatedError); + const newResult = mergeDataAndErrors(patch.data, patch.errors, onLocatedError); this.onNewResult(`${pathKey}.${index}`, newResult, this.asyncSelectionSets[patch.label]); }); }); diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index ffee9a28e81..31e6f813a3d 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -6,6 +6,7 @@ import { ExternalObject, MergedExecutionResult } from './types'; import { createExternalValue } from './externalValues'; import { + getInitialPath, getInitialPossibleFields, getReceiver, getSubschema, @@ -44,8 +45,9 @@ export function defaultMergedResolver( const data = parent[responseKey]; if (data !== undefined) { const unpathedErrors = getUnpathedErrors(parent); + const initialPath = getInitialPath(parent); const subschema = getSubschema(parent, responseKey); - return createExternalValue(data, unpathedErrors, subschema, context, info); + return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info); } } else if (info.fieldNodes[0].name.value in initialPossibleFields) { return resolveField(parent, responseKey, context, info); @@ -62,6 +64,7 @@ function resolveField( context: Record, info: GraphQLResolveInfo ): any { + const initialPath = getInitialPath(parent); const subschema = getSubschema(parent, responseKey); const receiver = getReceiver(parent, subschema); @@ -74,25 +77,25 @@ function resolveField( returnType: (info.returnType as GraphQLList).ofType, }; return mapAsyncIterator(asyncIterator as AsyncIterableIterator, ({ data, unpathedErrors }) => - createExternalValue(data, unpathedErrors, subschema, context, listMemberInfo, receiver)); + createExternalValue(data, unpathedErrors, initialPath, subschema, context, listMemberInfo, receiver)); }); } if (data === undefined) { return receiver.request(info).then(result => { const { data, unpathedErrors } = result as MergedExecutionResult; - return createExternalValue(data, unpathedErrors, subschema, context, info, receiver); + return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver); }); } const unpathedErrors = getUnpathedErrors(parent); receiver.update(info, { data, unpathedErrors }); - return createExternalValue(data, unpathedErrors, subschema, context, info, receiver); + return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver); } if (data !== undefined) { const unpathedErrors = getUnpathedErrors(parent); - return createExternalValue(data, unpathedErrors, subschema, context, info, receiver); + return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver); } // throw error? diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index e3c93bc6a8b..2c384f18e3a 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -10,6 +10,8 @@ import { DocumentNode, GraphQLOutputType, GraphQLObjectType, + responsePathAsArray, + GraphQLError, } from 'graphql'; import { ValueOrPromise } from 'value-or-promise'; @@ -120,6 +122,7 @@ function getDelegationContext({ rootValue, transforms = [], transformedSchema, + onLocatedError, }: IDelegateRequestOptions): DelegationContext { let operationDefinition: OperationDefinitionNode; let targetOperation: OperationTypeNode; @@ -161,6 +164,7 @@ function getDelegationContext({ ? subschemaOrSubschemaConfig.transforms.concat(transforms) : transforms, transformedSchema: transformedSchema ?? (subschemaOrSubschemaConfig as Subschema)?.transformedSchema ?? targetSchema, + onLocatedError: onLocatedError ?? ((error: GraphQLError) => error), asyncSelectionSets: Object.create(null), }; } @@ -235,7 +239,8 @@ function handleExecutionResult( return receiver.getInitialResult().then(({ data, unpathedErrors}) => { const { subschema, context, info, returnType } = delegationContext; - return createExternalValue(data, unpathedErrors, subschema, context, info, receiver, returnType); + const initialPath = responsePathAsArray(info.path); + return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver, returnType); }); } diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts index f39f217a079..cd747e88dfe 100644 --- a/packages/delegate/src/externalObjects.ts +++ b/packages/delegate/src/externalObjects.ts @@ -6,6 +6,7 @@ import { GraphQLResolveInfo, SelectionSetNode, locatedError, + responsePathAsArray, } from 'graphql'; import { relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils'; @@ -18,6 +19,7 @@ import { FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL, RECEIVER_MAP_SYMBOL, + INITIAL_PATH_SYMBOL, } from './symbols'; import { isSubschemaConfig } from './subschemaConfig'; import { Subschema } from './Subschema'; @@ -29,6 +31,7 @@ export function isExternalObject(data: any): data is ExternalObject { export function createExternalObject( object: any, errors: Array, + initialPath: Array, subschema: GraphQLSchema | SubschemaConfig, info: GraphQLResolveInfo, receiver: Receiver @@ -45,6 +48,7 @@ export function createExternalObject( 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 }, @@ -56,6 +60,10 @@ export function createExternalObject( return newObject; } +export function getInitialPath(object: ExternalObject): Array { + return object[INITIAL_PATH_SYMBOL]; +} + export function getSubschema(object: ExternalObject, responseKey?: string): GraphQLSchema | SubschemaConfig { return responseKey === undefined ? object[OBJECT_SUBSCHEMA_SYMBOL] @@ -79,13 +87,16 @@ export function getReceiver(object: ExternalObject, subschema: GraphQLSchema | S } export function mergeExternalObjects( - schema: GraphQLSchema, - path: ReadonlyArray, - typeName: string, target: ExternalObject, sources: Array, selectionSets: Array ): ExternalObject { + const initialPath = getInitialPath(target); + const parentInfo = getInfo(target); + const schema = parentInfo.schema; + const typeName = target.__typename; + const parentPath = responsePathAsArray(parentInfo.path); + if (target[FIELD_SUBSCHEMA_MAP_SYMBOL] == null) { target[FIELD_SUBSCHEMA_MAP_SYMBOL] = Object.create(null); } @@ -110,9 +121,14 @@ export function mergeExternalObjects( if (source instanceof Error || source === null) { Object.keys(fieldNodes).forEach(responseKey => { if (source instanceof GraphQLError) { - target[responseKey] = relocatedError(source, path.concat([responseKey])); + const basePath = parentPath.slice(initialPath.length); + const tailPath = source.path.length === parentPath.length ? [responseKey] : source.path.slice(initialPath.length); + const newPath = basePath.concat(tailPath); + target[responseKey] = relocatedError(source, newPath); } else if (source instanceof Error) { - target[responseKey] = locatedError(source, fieldNodes[responseKey], path.concat([responseKey])); + const basePath = parentPath.slice(initialPath.length); + const newPath = basePath.concat([responseKey]); + target[responseKey] = locatedError(source, fieldNodes[responseKey], newPath); } else { target[responseKey] = null; } diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index 86c5ecdb429..94ab6ccdc3b 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -15,7 +15,7 @@ import { import AggregateError from '@ardatan/aggregate-error'; -import { ExecutionResult } from '@graphql-tools/utils'; +import { ExecutionResult, relocatedError } from '@graphql-tools/utils'; import { DelegationContext, Receiver, SubschemaConfig } from './types'; import { createExternalObject } from './externalObjects'; @@ -30,20 +30,21 @@ export function externalValueFromResult( const data = originalResult.data?.[fieldName]; const errors = originalResult.errors ?? []; + const initialPath = info ? responsePathAsArray(info.path) : []; const { data: newData, unpathedErrors } = mergeDataAndErrors( data, errors, - info ? responsePathAsArray(info.path) : undefined, onLocatedError ); - return createExternalValue(newData, unpathedErrors, subschema, context, info, receiver, returnType); + return createExternalValue(newData, unpathedErrors, initialPath, subschema, context, info, receiver, returnType); } export function createExternalValue( data: any, unpathedErrors: Array = [], + initialPath: Array, subschema: GraphQLSchema | SubschemaConfig, context: Record, info: GraphQLResolveInfo, @@ -52,6 +53,10 @@ export function createExternalValue( ): any { const type = getNullableType(returnType); + if (data instanceof GraphQLError) { + return relocatedError(data, initialPath.concat(data.path)); + } + if (data instanceof Error) { return data; } @@ -63,9 +68,9 @@ export function createExternalValue( if (isLeafType(type)) { return type.parseValue(data); } else if (isCompositeType(type)) { - return createExternalObject(data, unpathedErrors, subschema, info, receiver); + return createExternalObject(data, unpathedErrors, initialPath, subschema, info, receiver); } else if (isListType(type)) { - return createExternalList(type, data, unpathedErrors, subschema, context, info, receiver); + return createExternalList(type, data, unpathedErrors, initialPath, subschema, context, info, receiver); } } @@ -73,6 +78,7 @@ function createExternalList( type: GraphQLList, list: Array, unpathedErrors: Array, + initialPath: Array, subschema: GraphQLSchema | SubschemaConfig, context: Record, info: GraphQLResolveInfo, @@ -83,6 +89,7 @@ function createExternalList( getNullableType(type.ofType), listMember, unpathedErrors, + initialPath, subschema, context, info, @@ -95,11 +102,16 @@ function createExternalListMember( type: GraphQLType, listMember: any, unpathedErrors: Array, + initialPath: Array, subschema: GraphQLSchema | SubschemaConfig, context: Record, info: GraphQLResolveInfo, receiver?: Receiver ): any { + if (listMember instanceof GraphQLError) { + return relocatedError(listMember, initialPath.concat(listMember.path)); + } + if (listMember instanceof Error) { return listMember; } @@ -111,9 +123,9 @@ function createExternalListMember( if (isLeafType(type)) { return type.parseValue(listMember); } else if (isCompositeType(type)) { - return createExternalObject(listMember, unpathedErrors, subschema, info, receiver); + return createExternalObject(listMember, unpathedErrors, initialPath, subschema, info, receiver); } else if (isListType(type)) { - return createExternalList(type, listMember, unpathedErrors, subschema, context, info, receiver); + return createExternalList(type, listMember, unpathedErrors, initialPath, subschema, context, info, receiver); } } diff --git a/packages/delegate/src/getMergedParent.ts b/packages/delegate/src/getMergedParent.ts index 30e422e35b6..21fb374ad8f 100644 --- a/packages/delegate/src/getMergedParent.ts +++ b/packages/delegate/src/getMergedParent.ts @@ -4,7 +4,6 @@ import { GraphQLResolveInfo, Kind, SelectionSetNode, - responsePathAsArray, getNamedType, print, GraphQLFieldMap, @@ -171,9 +170,6 @@ function getMergedParentsFromFieldNodes( const promise = Promise.resolve(maybePromise).then(result => mergeExternalObjects( - parentInfo.schema, - responsePathAsArray(parentInfo.path), - object.__typename, object, [result], [selectionSet] @@ -190,9 +186,6 @@ function getMergedParentsFromFieldNodes( .then(results => getMergedParentsFromFieldNodes( mergedTypeInfo, mergeExternalObjects( - parentInfo.schema, - responsePathAsArray(parentInfo.path), - object.__typename, object, results, Array.from(resultMap.values()) diff --git a/packages/delegate/src/symbols.ts b/packages/delegate/src/symbols.ts index 16ad52d3729..df467cf7cee 100644 --- a/packages/delegate/src/symbols.ts +++ b/packages/delegate/src/symbols.ts @@ -1,3 +1,4 @@ +export const INITIAL_PATH_SYMBOL = Symbol('initialPath'); export const UNPATHED_ERRORS_SYMBOL = Symbol('subschemaErrors'); export const OBJECT_SUBSCHEMA_SYMBOL = Symbol('initialSubschema'); export const INITIAL_POSSIBLE_FIELDS = Symbol('initialPossibleFields'); diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 8bff7ecef2d..b2b9a47e68a 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -18,6 +18,7 @@ import DataLoader from 'dataloader'; import { ExecutionParams, ExecutionResult, Executor, Request, Subscriber, TypeMap } from '@graphql-tools/utils'; import { + INITIAL_PATH_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL, @@ -213,6 +214,7 @@ export interface Receiver { export interface ExternalObject> { __typename: string; [key: string]: any; + [INITIAL_PATH_SYMBOL]: Array; [OBJECT_SUBSCHEMA_SYMBOL]: GraphQLSchema | SubschemaConfig; [INITIAL_POSSIBLE_FIELDS]: GraphQLFieldMap; [INFO_SYMBOL]: GraphQLResolveInfo; diff --git a/packages/wrap/src/generateProxyingResolvers.ts b/packages/wrap/src/generateProxyingResolvers.ts index 008726d4294..feda3bccb5b 100644 --- a/packages/wrap/src/generateProxyingResolvers.ts +++ b/packages/wrap/src/generateProxyingResolvers.ts @@ -2,15 +2,16 @@ import { GraphQLFieldResolver, GraphQLObjectType, GraphQLResolveInfo, GraphQLSch import { getResponseKeyFromInfo } from '@graphql-tools/utils'; import { - delegateToSchema, - getSubschema, - createExternalValue, - SubschemaConfig, ICreateProxyingResolverOptions, + SubschemaConfig, applySchemaTransforms, - isExternalObject, - getUnpathedErrors, + createExternalValue, + delegateToSchema, + getInitialPath, getReceiver, + getSubschema, + getUnpathedErrors, + isExternalObject, } from '@graphql-tools/delegate'; export function generateProxyingResolvers( @@ -83,8 +84,9 @@ function createPossiblyNestedProxyingResolver( // also nested as a field within a different type. if (subschemaConfig === subschema && parent[responseKey] !== undefined) { const unpathedErrors = getUnpathedErrors(parent); + const initialPath = getInitialPath(parent); const receiver = getReceiver(parent, subschema); - return createExternalValue(parent[responseKey], unpathedErrors, subschema, context, info, receiver); + return createExternalValue(parent[responseKey], unpathedErrors, initialPath, subschema, context, info, receiver); } } } From 826e84b1d201a57be0fadfdaf4cf77422b6e1320 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 24 May 2021 21:49:43 +0300 Subject: [PATCH 38/49] pare down receiver algorithm --- packages/delegate/src/InitialReceiver.ts | 90 ------------------------ 1 file changed, 90 deletions(-) diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index 6dfc43f92dc..60f6371245b 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -30,12 +30,9 @@ export class InitialReceiver implements Receiver { private readonly asyncSelectionSets: Record; private readonly resultTransformer: (originalResult: ExecutionResult) => any; private readonly initialResultDepth: number; - private deferredPatches: Record>; - private streamedPatches: Record>>; private cache: ExpectantStore; private stoppers: Array; private loaders: Record>; - private infos: Record>; constructor( asyncIterable: AsyncIterable, @@ -53,12 +50,9 @@ export class InitialReceiver implements Receiver { this.resultTransformer = resultTransformer; this.initialResultDepth = info ? responsePathAsArray(info.path).length - 1 : 0; - this.deferredPatches = Object.create(null); - this.streamedPatches = Object.create(null); this.cache = new ExpectantStore(); this.stoppers = []; this.loaders = Object.create(null); - this.infos = Object.create(null); } public async getInitialResult(): Promise { @@ -132,16 +126,6 @@ export class InitialReceiver implements Receiver { fieldNodes: [].concat(...infos.map(info => info.fieldNodes)), }; - let infosByParentKey = this.infos[parentKey]; - if (infosByParentKey === undefined) { - infosByParentKey = this.infos[parentKey] = Object.create(null); - } - - if (infosByParentKey[responseKey] === undefined) { - infosByParentKey[responseKey] = combinedInfo; - this.onNewInfo(pathKey); - } - const parent = this.cache.get(parentKey); if (parent === undefined) { @@ -228,24 +212,6 @@ export class InitialReceiver implements Receiver { const responseKey = parentPath.pop(); const parentPathKey = parentPath.join('.'); const pathKey = `${parentPathKey}.${responseKey}`; - const info = this.infos[parentPathKey]?.[responseKey]; - if (info === undefined) { - const streamedPatches = this.streamedPatches[pathKey]; - if (streamedPatches === undefined) { - this.streamedPatches[pathKey] = { [index]: [transformedResult] }; - continue; - } - - const indexPatches = streamedPatches[index]; - if (indexPatches === undefined) { - streamedPatches[index] = [transformedResult]; - continue; - } - - indexPatches.push(transformedResult); - continue; - } - const { onLocatedError } = this.delegationContext; const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); @@ -257,17 +223,6 @@ export class InitialReceiver implements Receiver { const responseKey = parentPath.pop(); const parentPathKey = parentPath.join('.'); const pathKey = `${parentPathKey}.${responseKey}`; - const info = this.infos[parentPathKey]?.[responseKey]; - if (info === undefined) { - const deferredPatches = this.deferredPatches[pathKey]; - if (deferredPatches === undefined) { - this.deferredPatches[pathKey] = [transformedResult]; - continue; - } - - deferredPatches.push(transformedResult); - continue; - } const { onLocatedError } = this.delegationContext; const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); @@ -294,51 +249,6 @@ export class InitialReceiver implements Receiver { ); this.cache.set(pathKey, mergedResult); - - const infosByParentKey = this.infos[pathKey]; - if (infosByParentKey !== undefined) { - const unpathedErrors = newResult.unpathedErrors; - Object.keys(infosByParentKey).forEach(responseKey => { - const info = infosByParentKey[responseKey]; - const data = newResult.data[responseKey]; - if (data !== undefined) { - const subResult = { data, unpathedErrors }; - const subPathKey = `${pathKey}.${responseKey}`; - this.onNewResult( - subPathKey, - subResult, - isCompositeType(getNamedType(info.returnType)) - ? { - kind: Kind.SELECTION_SET, - selections: [].concat(...info.fieldNodes.map(fieldNode => fieldNode.selectionSet.selections)), - } - : undefined - ); - } - }); - } - } - - private onNewInfo(pathKey: string): void { - const deferredPatches = this.deferredPatches[pathKey]; - if (deferredPatches !== undefined) { - deferredPatches.forEach(deferredPatch => { - const { onLocatedError } = this.delegationContext; - const newResult = mergeDataAndErrors(deferredPatch.data, deferredPatch.errors, onLocatedError); - this.onNewResult(pathKey, newResult, this.asyncSelectionSets[deferredPatch.label]); - }); - } - - const streamedPatches = this.streamedPatches[pathKey]; - if (streamedPatches !== undefined) { - Object.entries(streamedPatches).forEach(([index, indexPatches]) => { - indexPatches.forEach(patch => { - const { onLocatedError } = this.delegationContext; - const newResult = mergeDataAndErrors(patch.data, patch.errors, onLocatedError); - this.onNewResult(`${pathKey}.${index}`, newResult, this.asyncSelectionSets[patch.label]); - }); - }); - } } } From 0fcfc4fdce97166098a4781b73b943b4555f66a9 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 24 May 2021 22:09:37 +0300 Subject: [PATCH 39/49] simplify --- packages/delegate/src/InitialReceiver.ts | 39 ++++-------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/InitialReceiver.ts index 60f6371245b..f2dc25137da 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/InitialReceiver.ts @@ -194,40 +194,13 @@ export class InitialReceiver implements Receiver { const transformedResult = this.resultTransformer(asyncResult); - if (path.length === 1) { - const pathKey = path.join('.'); + const newResult = mergeDataAndErrors( + transformedResult.data, + transformedResult.errors, + this.delegationContext.onLocatedError + ); - const { onLocatedError } = this.delegationContext; - const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); - - this.onNewResult(pathKey, newResult, this.asyncSelectionSets[asyncResult.label]); - continue; - } - - const lastPathSegment = path[path.length - 1]; - const isStreamPatch = typeof lastPathSegment === 'number'; - if (isStreamPatch) { - const parentPath = path.slice(); - const index = parentPath.pop(); - const responseKey = parentPath.pop(); - const parentPathKey = parentPath.join('.'); - const pathKey = `${parentPathKey}.${responseKey}`; - const { onLocatedError } = this.delegationContext; - const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); - - this.onNewResult(`${pathKey}.${index}`, newResult, this.asyncSelectionSets[asyncResult.label]); - continue; - } - - const parentPath = path.slice(); - const responseKey = parentPath.pop(); - const parentPathKey = parentPath.join('.'); - const pathKey = `${parentPathKey}.${responseKey}`; - - const { onLocatedError } = this.delegationContext; - const newResult = mergeDataAndErrors(transformedResult.data, transformedResult.errors, onLocatedError); - - this.onNewResult(pathKey, newResult, this.asyncSelectionSets[asyncResult.label]); + this.onNewResult(path.join('.'), newResult, this.asyncSelectionSets[asyncResult.label]); } setTimeout(() => { From b18c428438953d5abe40c4e08c7a41971f91752c Mon Sep 17 00:00:00 2001 From: Michael Copland Date: Wed, 12 May 2021 15:05:35 +0100 Subject: [PATCH 40/49] Reproduction test case --- .../batch-delegate/tests/errorPaths.test.ts | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 packages/batch-delegate/tests/errorPaths.test.ts 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); + }); +}); From cfaea06cd4aea120cc759942cabe2dd04c8effe8 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 27 May 2021 23:53:03 +0300 Subject: [PATCH 41/49] temporarily (?) remove valuesFromResults --- packages/batch-delegate/src/createBatchDelegateFn.ts | 2 -- packages/batch-delegate/src/getLoader.ts | 12 +++--------- packages/batch-delegate/src/types.ts | 2 -- packages/batch-delegate/tests/withTransforms.test.ts | 2 +- packages/delegate/src/types.ts | 1 - packages/stitch/src/createMergedTypeResolver.ts | 3 +-- 6 files changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/batch-delegate/src/createBatchDelegateFn.ts b/packages/batch-delegate/src/createBatchDelegateFn.ts index 5b335063023..bd41f3a5d7b 100644 --- a/packages/batch-delegate/src/createBatchDelegateFn.ts +++ b/packages/batch-delegate/src/createBatchDelegateFn.ts @@ -8,14 +8,12 @@ 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); } diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 03a5bb8515c..11a4f5f4a76 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -14,23 +14,17 @@ const cache1: WeakMap< function createBatchFn(options: BatchDelegateOptions) { const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray) => ({ ids: keys })); - const { valuesFromResults, lazyOptionsFn } = options; + const { lazyOptionsFn } = options; return async (keys: ReadonlyArray) => { - const results = await delegateToSchema({ + const batchResult = await delegateToSchema({ returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)), args: argsFromKeys(keys), ...(lazyOptionsFn == null ? options : lazyOptionsFn(options)), }); - if (results instanceof Error) { - return keys.map(() => results); - } - - const values = valuesFromResults == null ? results : valuesFromResults(results, keys); - - return Array.isArray(values) ? values : keys.map(() => values); + return Array.isArray(batchResult) ? batchResult : keys.map(() => batchResult); }; } diff --git a/packages/batch-delegate/src/types.ts b/packages/batch-delegate/src/types.ts index c9601b5541a..2e8c96e3e83 100644 --- a/packages/batch-delegate/src/types.ts +++ b/packages/batch-delegate/src/types.ts @@ -15,7 +15,6 @@ export interface BatchDelegateOptions, K = any, V dataLoaderOptions?: DataLoader.Options; key: K; argsFromKeys?: (keys: ReadonlyArray) => Record; - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array; lazyOptionsFn?: BatchDelegateOptionsFn; } @@ -23,6 +22,5 @@ export interface CreateBatchDelegateFnOptions, K extends Partial, 'args' | 'info'>> { dataLoaderOptions?: DataLoader.Options; argsFromKeys?: (keys: ReadonlyArray) => Record; - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array; lazyOptionsFn?: (batchDelegateOptions: BatchDelegateOptions) => IDelegateToSchemaOptions; } diff --git a/packages/batch-delegate/tests/withTransforms.test.ts b/packages/batch-delegate/tests/withTransforms.test.ts index e204dcf8db6..17189edf9d0 100644 --- a/packages/batch-delegate/tests/withTransforms.test.ts +++ b/packages/batch-delegate/tests/withTransforms.test.ts @@ -6,7 +6,7 @@ import { stitchSchemas } from '@graphql-tools/stitch'; import { TransformQuery } from '@graphql-tools/wrap' describe('works with complex transforms', () => { - test('using TransformQuery instead of valuesFromResults', async () => { + test('using TransformQuery', async () => { const bookSchema = makeExecutableSchema({ typeDefs: ` type Book { diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index b2b9a47e68a..91f3588b0ef 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -176,7 +176,6 @@ export interface MergedTypeResolverOptions { fieldName?: string; args?: (originalResult: any) => Record; argsFromKeys?: (keys: ReadonlyArray) => Record; - valuesFromResults?: (results: any, keys: ReadonlyArray) => Array; } export interface MergedFieldConfig { diff --git a/packages/stitch/src/createMergedTypeResolver.ts b/packages/stitch/src/createMergedTypeResolver.ts index 6d010ab6c57..05a8fabf9b4 100644 --- a/packages/stitch/src/createMergedTypeResolver.ts +++ b/packages/stitch/src/createMergedTypeResolver.ts @@ -3,7 +3,7 @@ import { delegateToSchema, MergedTypeResolver, MergedTypeResolverOptions } from import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; export function createMergedTypeResolver(mergedTypeResolverOptions: MergedTypeResolverOptions): MergedTypeResolver { - const { fieldName, argsFromKeys, valuesFromResults, args } = mergedTypeResolverOptions; + const { fieldName, argsFromKeys, args } = mergedTypeResolverOptions; if (argsFromKeys != null) { return (originalResult, context, info, subschema, selectionSet, key) => @@ -16,7 +16,6 @@ export function createMergedTypeResolver(mergedTypeResolverOptions: MergedTypeRe ), key, argsFromKeys, - valuesFromResults, selectionSet, context, info, From c00442918a10aa063909c4da94f33549eae81d3d Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 28 May 2021 00:21:55 +0300 Subject: [PATCH 42/49] inline delegation into batch-delegate to make some better magic --- packages/batch-delegate/src/getLoader.ts | 84 +++++++++++++++++++++-- packages/delegate/src/delegateToSchema.ts | 6 +- packages/delegate/src/index.ts | 2 + 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 11a4f5f4a76..a9ada864989 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -1,9 +1,20 @@ -import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode } from 'graphql'; +import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode, responsePathAsArray } from 'graphql'; import DataLoader from 'dataloader'; -import { delegateToSchema, SubschemaConfig } from '@graphql-tools/delegate'; -import { relocatedError } from '@graphql-tools/utils'; +import { + SubschemaConfig, + Transformer, + createRequestFromInfo, + getDelegationContext, + getDelegatingOperation, + getExecutor, + validateRequest, + InitialReceiver, + createExternalValue, + externalValueFromResult, +} from '@graphql-tools/delegate'; +import { isAsyncIterable, relocatedError } from '@graphql-tools/utils'; import { BatchDelegateOptions } from './types'; @@ -17,14 +28,73 @@ function createBatchFn(options: BatchDelegateOptions) { const { lazyOptionsFn } = options; return async (keys: ReadonlyArray) => { - const batchResult = await delegateToSchema({ - returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), - onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)), + const { + context, + info, + operationName, + operation = getDelegatingOperation(info.parentType, info.schema), + fieldName = info.fieldName, + returnType = new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), + selectionSet, + fieldNodes, + binding, + skipValidation, + } = options; + + if (operation !== 'query' && operation !== 'mutation') { + throw new Error(`Batch delegation not possible for operation '${operation}'.`) + } + + const request = createRequestFromInfo({ + info, + operation, + fieldName, + selectionSet, + fieldNodes, + operationName, + }); + + const delegationContext = getDelegationContext({ + request, args: argsFromKeys(keys), + onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)), ...(lazyOptionsFn == null ? options : lazyOptionsFn(options)), + operation, + fieldName, + returnType, }); - return Array.isArray(batchResult) ? batchResult : keys.map(() => batchResult); + const transformer = new Transformer(delegationContext, binding); + + const processedRequest = transformer.transformRequest(request); + + if (!skipValidation) { + validateRequest(delegationContext, processedRequest.document); + } + + const executor = getExecutor(delegationContext); + + const batchResult = await executor({ + ...processedRequest, + context, + info + }); + + if (isAsyncIterable(batchResult)) { + const receiver = new InitialReceiver(batchResult, delegationContext, executionResult => transformer.transformResult(executionResult)); + + const { data, unpathedErrors } = await receiver.getInitialResult(); + + const { subschema, context, info, returnType } = delegationContext; + const initialPath = responsePathAsArray(info.path); + const batchValue = createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver, returnType); + + return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); + } + + const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext); + + return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); }; } diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 2c384f18e3a..821055bb01b 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -110,7 +110,7 @@ export function delegateRequest, TArgs = any>(opt const emptyObject = {}; -function getDelegationContext({ +export function getDelegationContext({ request, schema, operation, @@ -186,7 +186,7 @@ function getDelegationContext({ }; } -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) { @@ -209,7 +209,7 @@ const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValu })) as Executor; }); -function getExecutor(delegationContext: DelegationContext): Executor { +export function getExecutor(delegationContext: DelegationContext): Executor { const { subschemaConfig, targetSchema, context, rootValue } = delegationContext; let executor: Executor = diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 09a507f84af..e836011d10c 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -1,4 +1,6 @@ +export * from './InitialReceiver'; export * from './Subschema'; +export * from './Transformer'; export * from './applySchemaTransforms'; export * from './createRequest'; export * from './defaultMergedResolver'; From 489c9bf82b9b123b9d1cfc42d02fea24e525c9bb Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 28 May 2021 00:27:48 +0300 Subject: [PATCH 43/49] add TODOs by splitting prior to externalValue creation we will be able to use the correct info --- packages/batch-delegate/src/getLoader.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index a9ada864989..54f4b089f9d 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -81,6 +81,8 @@ function createBatchFn(options: BatchDelegateOptions) { }); if (isAsyncIterable(batchResult)) { + // TODO: split the asyncIterable and make a new receiver from each of them, return the Receiver instead of the + // initial value, so that the correct info can be used to instantiate the Receiver const receiver = new InitialReceiver(batchResult, delegationContext, executionResult => transformer.transformResult(executionResult)); const { data, unpathedErrors } = await receiver.getInitialResult(); @@ -92,7 +94,9 @@ function createBatchFn(options: BatchDelegateOptions) { return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); } - const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext); + // TODO: split the batchedResult and return the result instead of the value, so the correct info + // can be used to instantiate the value + const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext); return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); }; From 99bda2d245ffbd3f9db63e012562b575ff69e5c1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 30 May 2021 00:05:59 +0300 Subject: [PATCH 44/49] Receiver refactor We may end up having one type of Receiver Receivers can also return external values not just MergedExecutionResults, even if the cache is of MergedExecutionsResults --- packages/batch-delegate/src/getLoader.ts | 25 ++++--- .../src/{InitialReceiver.ts => Receiver.ts} | 47 ++++++------- packages/delegate/src/delegateToSchema.ts | 66 +++++++++++-------- packages/delegate/src/externalObjects.ts | 6 +- packages/delegate/src/externalValues.ts | 9 +-- packages/delegate/src/index.ts | 2 +- packages/delegate/src/types.ts | 14 ++-- 7 files changed, 89 insertions(+), 80 deletions(-) rename packages/delegate/src/{InitialReceiver.ts => Receiver.ts} (85%) diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 54f4b089f9d..536efdd8360 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -1,4 +1,4 @@ -import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode, responsePathAsArray } from 'graphql'; +import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode } from 'graphql'; import DataLoader from 'dataloader'; @@ -10,8 +10,7 @@ import { getDelegatingOperation, getExecutor, validateRequest, - InitialReceiver, - createExternalValue, + Receiver, externalValueFromResult, } from '@graphql-tools/delegate'; import { isAsyncIterable, relocatedError } from '@graphql-tools/utils'; @@ -42,7 +41,7 @@ function createBatchFn(options: BatchDelegateOptions) { } = options; if (operation !== 'query' && operation !== 'mutation') { - throw new Error(`Batch delegation not possible for operation '${operation}'.`) + throw new Error(`Batch delegation not possible for operation '${operation}'.`); } const request = createRequestFromInfo({ @@ -77,26 +76,24 @@ function createBatchFn(options: BatchDelegateOptions) { const batchResult = await executor({ ...processedRequest, context, - info + info, }); if (isAsyncIterable(batchResult)) { // TODO: split the asyncIterable and make a new receiver from each of them, return the Receiver instead of the // initial value, so that the correct info can be used to instantiate the Receiver - const receiver = new InitialReceiver(batchResult, delegationContext, executionResult => transformer.transformResult(executionResult)); - - const { data, unpathedErrors } = await receiver.getInitialResult(); + const receiver = new Receiver(batchResult, delegationContext, executionResult => + transformer.transformResult(executionResult) + ); - const { subschema, context, info, returnType } = delegationContext; - const initialPath = responsePathAsArray(info.path); - const batchValue = createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver, returnType); + const batchValue = await receiver.getInitialValue(); return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); } - // TODO: split the batchedResult and return the result instead of the value, so the correct info - // can be used to instantiate the value - const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext); + // TODO: split the batchedResult and return the result instead of the value, so the correct info + // can be used to instantiate the value + const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext); return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); }; diff --git a/packages/delegate/src/InitialReceiver.ts b/packages/delegate/src/Receiver.ts similarity index 85% rename from packages/delegate/src/InitialReceiver.ts rename to packages/delegate/src/Receiver.ts index f2dc25137da..1f7965875a0 100644 --- a/packages/delegate/src/InitialReceiver.ts +++ b/packages/delegate/src/Receiver.ts @@ -16,14 +16,20 @@ import DataLoader from 'dataloader'; import { Repeater, Stop } from '@repeaterjs/repeater'; -import { AsyncExecutionResult, collectFields, getResponseKeyFromInfo, GraphQLExecutionContext } from '@graphql-tools/utils'; +import { + AsyncExecutionResult, + collectFields, + getResponseKeyFromInfo, + GraphQLExecutionContext, +} from '@graphql-tools/utils'; -import { DelegationContext, MergedExecutionResult, Receiver } from './types'; +import { DelegationContext, MergedExecutionResult } from './types'; import { mergeDataAndErrors } from './mergeDataAndErrors'; import { ExpectantStore } from './expectantStore'; import { fieldShouldStream } from './fieldShouldStream'; +import { createExternalValue } from './externalValues'; -export class InitialReceiver implements Receiver { +export class Receiver { private readonly asyncIterable: AsyncIterable; private readonly delegationContext: DelegationContext; private readonly fieldName: string; @@ -55,8 +61,8 @@ export class InitialReceiver implements Receiver { this.loaders = Object.create(null); } - public async getInitialResult(): Promise { - const { fieldName, info, onLocatedError } = this.delegationContext; + public async getInitialValue(): Promise { + const { subschema, fieldName, context, info, returnType, onLocatedError } = this.delegationContext; let initialResult: ExecutionResult; let initialData: any; @@ -73,7 +79,9 @@ export class InitialReceiver implements Receiver { this._iterate(); - return newResult; + const { data, unpathedErrors } = newResult; + const initialPath = responsePathAsArray(info.path); + return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, this, returnType); } public update(info: GraphQLResolveInfo, result: MergedExecutionResult): void { @@ -83,11 +91,7 @@ export class InitialReceiver implements Receiver { this._update(info, result, pathKey); } - private _update( - info: GraphQLResolveInfo, - result: MergedExecutionResult, - pathKey: string, - ): void { + private _update(info: GraphQLResolveInfo, result: MergedExecutionResult, pathKey: string): void { this.onNewResult( pathKey, result, @@ -100,7 +104,9 @@ export class InitialReceiver implements Receiver { ); } - public request(info: GraphQLResolveInfo): Promise { + public request( + info: GraphQLResolveInfo + ): Promise> { const path = responsePathAsArray(info.path).slice(this.initialResultDepth); const pathKey = path.join('.'); let loader = this.loaders[pathKey]; @@ -129,7 +135,7 @@ export class InitialReceiver implements Receiver { const parent = this.cache.get(parentKey); if (parent === undefined) { - throw new Error(`Parent with key "${parentKey}" not available.`) + throw new Error(`Parent with key "${parentKey}" not available.`); } const data = parent.data[responseKey]; @@ -211,15 +217,10 @@ export class InitialReceiver implements Receiver { private onNewResult(pathKey: string, newResult: MergedExecutionResult, selectionSet: SelectionSetNode): void { const result = this.cache.get(pathKey); - const mergedResult = result === undefined - ? newResult - : mergeResults( - this.delegationContext.info.schema, - result.data.__typename, - result, - newResult, - selectionSet - ); + const mergedResult = + result === undefined + ? newResult + : mergeResults(this.delegationContext.info.schema, result.data.__typename, result, newResult, selectionSet); this.cache.set(pathKey, mergedResult); } @@ -252,7 +253,7 @@ export function mergeResults( }); } - target.unpathedErrors.push(...source.unpathedErrors ?? []); + target.unpathedErrors.push(...(source.unpathedErrors ?? [])); return target; } diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 821055bb01b..add838dbe3c 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -10,7 +10,6 @@ import { DocumentNode, GraphQLOutputType, GraphQLObjectType, - responsePathAsArray, GraphQLError, } from 'graphql'; @@ -44,8 +43,8 @@ import { Subschema } from './Subschema'; import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; import { Transformer } from './Transformer'; import { memoize2 } from './memoize'; -import { InitialReceiver } from './InitialReceiver'; -import { createExternalValue, externalValueFromResult } from './externalValues'; +import { Receiver } from './Receiver'; +import { externalValueFromResult } from './externalValues'; import { defaultDelegationBinding } from './delegationBindings'; export function delegateToSchema, TArgs = any>( @@ -96,7 +95,9 @@ function getDelegationReturnType( return rootType.getFields()[fieldName].type; } -export function delegateRequest, TArgs = any>(options: IDelegateRequestOptions) { +export function delegateRequest, TArgs = any>( + options: IDelegateRequestOptions +) { const delegationContext = getDelegationContext(options); const operation = delegationContext.operation; @@ -137,7 +138,7 @@ export function getDelegationContext({ if (fieldName == null) { operationDefinition = operationDefinition ?? getOperationAST(request.document, undefined); - targetFieldName = ((operationDefinition.selectionSet.selections[0] as unknown) as FieldDefinitionNode).name.value; + targetFieldName = (operationDefinition.selectionSet.selections[0] as unknown as FieldDefinitionNode).name.value; } else { targetFieldName = fieldName; } @@ -158,12 +159,14 @@ export function getDelegationContext({ context, info, rootValue: rootValue ?? subschemaOrSubschemaConfig?.rootValue ?? info?.rootValue ?? emptyObject, - returnType: returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, targetOperation, targetFieldName), + returnType: + returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, targetOperation, targetFieldName), transforms: subschemaOrSubschemaConfig.transforms != null ? subschemaOrSubschemaConfig.transforms.concat(transforms) : transforms, - transformedSchema: transformedSchema ?? (subschemaOrSubschemaConfig as Subschema)?.transformedSchema ?? targetSchema, + transformedSchema: + transformedSchema ?? (subschemaOrSubschemaConfig as Subschema)?.transformedSchema ?? targetSchema, onLocatedError: onLocatedError ?? ((error: GraphQLError) => error), asyncSelectionSets: Object.create(null), }; @@ -179,7 +182,10 @@ export function getDelegationContext({ context, info, rootValue: rootValue ?? info?.rootValue ?? emptyObject, - returnType: returnType ?? info?.returnType ?? getDelegationReturnType(subschemaOrSubschemaConfig, targetOperation, targetFieldName), + returnType: + returnType ?? + info?.returnType ?? + getDelegationReturnType(subschemaOrSubschemaConfig, targetOperation, targetFieldName), transforms, transformedSchema: transformedSchema ?? subschemaOrSubschemaConfig, asyncSelectionSets: Object.create(null), @@ -235,19 +241,20 @@ function handleExecutionResult( resultTransformer: (originalResult: ExecutionResult) => ExecutionResult ): any { if (isAsyncIterable(executionResult)) { - const receiver = new InitialReceiver(executionResult, delegationContext, resultTransformer); + const receiver = new Receiver(executionResult, delegationContext, resultTransformer); - return receiver.getInitialResult().then(({ data, unpathedErrors}) => { - const { subschema, context, info, returnType } = delegationContext; - const initialPath = responsePathAsArray(info.path); - return createExternalValue(data, unpathedErrors, initialPath, subschema, context, info, receiver, returnType); - }); + return receiver.getInitialValue(); } return externalValueFromResult(resultTransformer(executionResult), delegationContext); } -export function delegateQueryOrMutation(request: Request, delegationContext: DelegationContext, skipValidation?: boolean, binding: DelegationBinding = defaultDelegationBinding) { +export function delegateQueryOrMutation( + request: Request, + delegationContext: DelegationContext, + skipValidation?: boolean, + binding: DelegationBinding = defaultDelegationBinding +) { const transformer = new Transformer(delegationContext, binding); const processedRequest = transformer.transformRequest(request); @@ -260,17 +267,19 @@ export function delegateQueryOrMutation(request: Request, delegationContext: Del const executor = getExecutor(delegationContext); - return new ValueOrPromise(() => executor({ - ...processedRequest, - context, - info - })).then( - executionResult => handleExecutionResult( - executionResult, - delegationContext, - originalResult => transformer.transformResult(originalResult) + return new ValueOrPromise(() => + executor({ + ...processedRequest, + context, + info, + }) + ) + .then(executionResult => + handleExecutionResult(executionResult, delegationContext, originalResult => + transformer.transformResult(originalResult) + ) ) - ).resolve(); + .resolve(); } function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record): Subscriber { @@ -304,7 +313,12 @@ function handleSubscriptionResult( return resultTransformer(subscriptionResult); } -export function delegateSubscription(request: Request, delegationContext: DelegationContext, skipValidation = false, binding = defaultDelegationBinding) { +export function delegateSubscription( + request: Request, + delegationContext: DelegationContext, + skipValidation = false, + binding = defaultDelegationBinding +) { const transformer = new Transformer(delegationContext, binding); const processedRequest = transformer.transformRequest(request); diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts index cd747e88dfe..11339bdedd2 100644 --- a/packages/delegate/src/externalObjects.ts +++ b/packages/delegate/src/externalObjects.ts @@ -11,7 +11,7 @@ import { import { relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils'; -import { SubschemaConfig, ExternalObject, Receiver } from './types'; +import { SubschemaConfig, ExternalObject } from './types'; import { OBJECT_SUBSCHEMA_SYMBOL, INITIAL_POSSIBLE_FIELDS, @@ -22,6 +22,7 @@ import { INITIAL_PATH_SYMBOL, } from './symbols'; import { isSubschemaConfig } from './subschemaConfig'; +import { Receiver } from './Receiver'; import { Subschema } from './Subschema'; export function isExternalObject(data: any): data is ExternalObject { @@ -122,7 +123,8 @@ export function mergeExternalObjects( Object.keys(fieldNodes).forEach(responseKey => { if (source instanceof GraphQLError) { const basePath = parentPath.slice(initialPath.length); - const tailPath = source.path.length === parentPath.length ? [responseKey] : source.path.slice(initialPath.length); + const tailPath = + source.path.length === parentPath.length ? [responseKey] : source.path.slice(initialPath.length); const newPath = basePath.concat(tailPath); target[responseKey] = relocatedError(source, newPath); } else if (source instanceof Error) { diff --git a/packages/delegate/src/externalValues.ts b/packages/delegate/src/externalValues.ts index 94ab6ccdc3b..0b82342a437 100644 --- a/packages/delegate/src/externalValues.ts +++ b/packages/delegate/src/externalValues.ts @@ -17,9 +17,10 @@ import AggregateError from '@ardatan/aggregate-error'; import { ExecutionResult, relocatedError } from '@graphql-tools/utils'; -import { DelegationContext, Receiver, SubschemaConfig } from './types'; +import { DelegationContext, SubschemaConfig } from './types'; import { createExternalObject } from './externalObjects'; import { mergeDataAndErrors } from './mergeDataAndErrors'; +import { Receiver } from './Receiver'; export function externalValueFromResult( originalResult: ExecutionResult, @@ -32,11 +33,7 @@ export function externalValueFromResult( const errors = originalResult.errors ?? []; const initialPath = info ? responsePathAsArray(info.path) : []; - const { data: newData, unpathedErrors } = mergeDataAndErrors( - data, - errors, - onLocatedError - ); + const { data: newData, unpathedErrors } = mergeDataAndErrors(data, errors, onLocatedError); return createExternalValue(newData, unpathedErrors, initialPath, subschema, context, info, receiver, returnType); } diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index e836011d10c..248a2062f4e 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -1,4 +1,4 @@ -export * from './InitialReceiver'; +export * from './Receiver'; export * from './Subschema'; export * from './Transformer'; export * from './applySchemaTransforms'; diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 91f3588b0ef..fc73d45323e 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -27,6 +27,7 @@ import { INFO_SYMBOL, } from './symbols'; +import { Receiver } from './Receiver'; import { Subschema } from './Subschema'; export type SchemaTransform = ( @@ -60,7 +61,7 @@ export interface DelegationContext { args: Record; context: Record; info: GraphQLResolveInfo; - rootValue?: Record, + rootValue?: Record; returnType: GraphQLOutputType; onLocatedError?: (originalError: GraphQLError) => GraphQLError; transforms: Array; @@ -159,14 +160,16 @@ export interface SubschemaConfig; } -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; @@ -205,11 +208,6 @@ export interface MergedExecutionResult> { data: TData; } -export interface Receiver { - request: (info: GraphQLResolveInfo) => Promise>; - update: (info: GraphQLResolveInfo, result: MergedExecutionResult) => void; -} - export interface ExternalObject> { __typename: string; [key: string]: any; From df69614cd6026c77533026dfab06b9cf7f622628 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 1 Jun 2021 09:08:08 +0300 Subject: [PATCH 45/49] hew more closely to repeater js implementation https://github.com/repeaterjs/repeater/issues/48#issuecomment-569134039 add additional tests --- packages/utils/src/mapAsyncIterator.ts | 60 +++++-------------- packages/utils/src/splitAsyncIterator.ts | 47 +++++---------- .../utils/tests/splitAsyncIterator.spec.ts | 48 +++++++++++++++ 3 files changed, 78 insertions(+), 77 deletions(-) diff --git a/packages/utils/src/mapAsyncIterator.ts b/packages/utils/src/mapAsyncIterator.ts index 8a28c971ea0..c70854c5a40 100644 --- a/packages/utils/src/mapAsyncIterator.ts +++ b/packages/utils/src/mapAsyncIterator.ts @@ -7,13 +7,13 @@ * so that all payloads will be delivered in the original order */ -import { Push, Stop, Repeater } from '@repeaterjs/repeater'; +import { Repeater } from '@repeaterjs/repeater'; export function mapAsyncIterator( iterator: AsyncIterator, - mapValue: (value: T) => Promise | U, + mapValue: (value: T) => Promise | U ): AsyncIterableIterator { - const returner = iterator.return?.bind(iterator) ?? (() => {}); + const returner = iterator.return?.bind(iterator) ?? (() => true); return new Repeater(async (push, stop) => { let earlyReturn: any; @@ -21,51 +21,19 @@ export function mapAsyncIterator( earlyReturn = returner(); }); - await loop(push, stop, earlyReturn, iterator, mapValue); + /* eslint-disable no-unmodified-loop-condition */ + while (!earlyReturn) { + const iteration = await iterator.next(); - await earlyReturn; - }); -} - -async function loop( - push: Push, - stop: Stop, - earlyReturn: Promise | any, - iterator: AsyncIterator, - mapValue: (value: T) => Promise | U, -): Promise { - /* eslint-disable no-unmodified-loop-condition */ - while (!earlyReturn) { - const iteration = await next(iterator, mapValue); - - if (iteration.done) { - if (iteration.value !== undefined) { - await push(iteration.value); + if (iteration.done) { + stop(); + return iteration.value; } - stop(); - return; - } - await push(iteration.value); - } - /* eslint-enable no-unmodified-loop-condition */ -} - -async function next( - iterator: AsyncIterator, - mapValue: (value: T) => Promise | U, -): Promise> { - const iterationCandidate = await iterator.next(); - - const value = iterationCandidate.value; - if (value === undefined) { - return iterationCandidate as IteratorResult; - } - - const newValue = await mapValue(iterationCandidate.value); + await push(mapValue(iteration.value)); + } + /* eslint-enable no-unmodified-loop-condition */ - return { - ...iterationCandidate, - value: newValue, - }; + await earlyReturn; + }); } diff --git a/packages/utils/src/splitAsyncIterator.ts b/packages/utils/src/splitAsyncIterator.ts index eafff5fca8e..709bb9d90e8 100644 --- a/packages/utils/src/splitAsyncIterator.ts +++ b/packages/utils/src/splitAsyncIterator.ts @@ -2,12 +2,12 @@ // and: https://gist.github.com/jed/cc1e949419d42e2cb26d7f2e1645864d // and also: https://github.com/repeaterjs/repeater/issues/48#issuecomment-569134039 -import { Push, Stop, Repeater } from '@repeaterjs/repeater'; +import { Repeater } from '@repeaterjs/repeater'; type Splitter = (item: T) => [number | undefined, T]; export function splitAsyncIterator(iterator: AsyncIterator, n: number, splitter: Splitter) { - const returner = iterator.return?.bind(iterator) ?? (() => {}); + const returner = iterator.return?.bind(iterator) ?? (() => true); const buffers: Array>> = Array(n); for (let i = 0; i < n; i++) { @@ -26,41 +26,26 @@ export function splitAsyncIterator(iterator: AsyncIterator, n: number, spl } }); - await loop(push, stop, earlyReturn, buffer, buffers, iterator, splitter); + /* eslint-disable no-unmodified-loop-condition */ + while (!earlyReturn) { + const iteration = await next(buffer, buffers, iterator, splitter); - await earlyReturn; - }); - }); -} - -async function loop( - push: Push, - stop: Stop, - earlyReturn: Promise | any, - buffer: Array>, - buffers: Array>>, - iterator: AsyncIterator, - splitter: Splitter -): Promise { - /* eslint-disable no-unmodified-loop-condition */ - while (!earlyReturn) { - const iteration = await next(buffer, buffers, iterator, splitter); + if (iteration === undefined) { + continue; + } - if (iteration === undefined) { - continue; - } + if (iteration.done) { + stop(); + return iteration.value; + } - if (iteration.done) { - if (iteration.value !== undefined) { await push(iteration.value); } - stop(); - return; - } + /* eslint-enable no-unmodified-loop-condition */ - await push(iteration.value); - } - /* eslint-enable no-unmodified-loop-condition */ + await earlyReturn; + }); + }); } async function next( diff --git a/packages/utils/tests/splitAsyncIterator.spec.ts b/packages/utils/tests/splitAsyncIterator.spec.ts index be4de47d004..a92a77da091 100644 --- a/packages/utils/tests/splitAsyncIterator.spec.ts +++ b/packages/utils/tests/splitAsyncIterator.spec.ts @@ -1,4 +1,5 @@ import { splitAsyncIterator } from '../src/splitAsyncIterator'; +import { mapAsyncIterator } from '../src/mapAsyncIterator'; describe('splitAsyncIterator', () => { test('it works sequentially', async () => { @@ -44,3 +45,50 @@ describe('splitAsyncIterator', () => { expect(twoResults).toEqual([undefined, undefined, undefined]); }); }); + +describe('splitAsyncIterator with mapAsyncIterator', () => { + test('it works sequentially', async () => { + const gen3 = async function* () { + for (let i = 0; i < 3; i++) { + yield i; + } + }(); + + const mappedGen3 = mapAsyncIterator(gen3, value => value); + const [one, two] = splitAsyncIterator(mappedGen3, 2, (x) => [0, x + 5]); + + let results = []; + for await (const result of one) { + results.push(result); + } + expect(results).toEqual([5, 6, 7]); + + results = []; + for await (const result of two) { + results.push(result); + } + expect(results).toEqual([]); + }); + + test('it works in parallel', async () => { + const gen3 = async function* () { + for (let i = 0; i < 3; i++) { + yield i; + } + }(); + + const mappedGen3 = mapAsyncIterator(gen3, value => value); + const [one, two] = splitAsyncIterator(mappedGen3, 2, (x) => [0, x + 5]); + + const oneResults = []; + const twoResults = []; + for (let i = 0; i < 3; i++) { + const results = await Promise.all([one.next(), two.next()]); + oneResults.push(results[0].value); + twoResults.push(results[1].value); + } + + expect(oneResults).toEqual([5, 6, 7]); + expect(twoResults).toEqual([undefined, undefined, undefined]); + }); +}); From 97c0328c8a032121e24413f379396789ecf6786c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 1 Jun 2021 10:31:45 +0300 Subject: [PATCH 46/49] Receiver does not have to transform results It can be passed a stream of transformedResults using mapAsyncIterator note that using `break` in for await(...of...) causes abrupt completion and will return the iterator, not really sure why this became a problem only when mapping the iterator --- packages/delegate/src/Receiver.ts | 34 ++++++------------- packages/delegate/src/delegateToSchema.ts | 4 +-- .../wrap/src/transforms/TransformQuery.ts | 22 +++++++++--- packages/wrap/src/transforms/WrapQuery.ts | 1 + 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/delegate/src/Receiver.ts b/packages/delegate/src/Receiver.ts index 1f7965875a0..e3d67c5b274 100644 --- a/packages/delegate/src/Receiver.ts +++ b/packages/delegate/src/Receiver.ts @@ -1,5 +1,4 @@ import { - ExecutionPatchResult, ExecutionResult, getNamedType, GraphQLObjectType, @@ -30,22 +29,17 @@ import { fieldShouldStream } from './fieldShouldStream'; import { createExternalValue } from './externalValues'; export class Receiver { - private readonly asyncIterable: AsyncIterable; + private readonly asyncIterator: AsyncIterator; private readonly delegationContext: DelegationContext; private readonly fieldName: string; private readonly asyncSelectionSets: Record; - private readonly resultTransformer: (originalResult: ExecutionResult) => any; private readonly initialResultDepth: number; private cache: ExpectantStore; private stoppers: Array; private loaders: Record>; - constructor( - asyncIterable: AsyncIterable, - delegationContext: DelegationContext, - resultTransformer: (originalResult: ExecutionResult) => any - ) { - this.asyncIterable = asyncIterable; + constructor(asyncIterator: AsyncIterator, delegationContext: DelegationContext) { + this.asyncIterator = asyncIterator; this.delegationContext = delegationContext; const { fieldName, info, asyncSelectionSets } = delegationContext; @@ -53,7 +47,6 @@ export class Receiver { this.fieldName = fieldName; this.asyncSelectionSets = asyncSelectionSets; - this.resultTransformer = resultTransformer; this.initialResultDepth = info ? responsePathAsArray(info.path).length - 1 : 0; this.cache = new ExpectantStore(); @@ -66,12 +59,13 @@ export class Receiver { let initialResult: ExecutionResult; let initialData: any; - for await (const payload of this.asyncIterable) { - initialResult = this.resultTransformer(payload); - initialData = initialResult?.data?.[fieldName]; - if (initialData != null) { + while (initialData == null) { + const payload = await this.asyncIterator.next(); + if (payload.done) { break; } + initialResult = payload.value; + initialData = initialResult?.data?.[fieldName]; } const newResult = mergeDataAndErrors(initialData, initialResult.errors, onLocatedError); @@ -178,11 +172,9 @@ export class Receiver { } private async _iterate(): Promise { - const iterator = this.asyncIterable[Symbol.asyncIterator](); - let hasNext = true; while (hasNext) { - const payload = (await iterator.next()) as IteratorResult; + const payload = await this.asyncIterator.next(); hasNext = !payload.done; const asyncResult = payload.value; @@ -198,13 +190,7 @@ export class Receiver { continue; } - const transformedResult = this.resultTransformer(asyncResult); - - const newResult = mergeDataAndErrors( - transformedResult.data, - transformedResult.errors, - this.delegationContext.onLocatedError - ); + const newResult = mergeDataAndErrors(asyncResult.data, asyncResult.errors, this.delegationContext.onLocatedError); this.onNewResult(path.join('.'), newResult, this.asyncSelectionSets[asyncResult.label]); } diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index add838dbe3c..86c9ecbfee4 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -241,8 +241,8 @@ function handleExecutionResult( resultTransformer: (originalResult: ExecutionResult) => ExecutionResult ): any { if (isAsyncIterable(executionResult)) { - const receiver = new Receiver(executionResult, delegationContext, resultTransformer); - + const transformedIterable = mapAsyncIterator(executionResult, value => resultTransformer(value)); + const receiver = new Receiver(transformedIterable, delegationContext); return receiver.getInitialValue(); } diff --git a/packages/wrap/src/transforms/TransformQuery.ts b/packages/wrap/src/transforms/TransformQuery.ts index 06221726df6..717e2ec1033 100644 --- a/packages/wrap/src/transforms/TransformQuery.ts +++ b/packages/wrap/src/transforms/TransformQuery.ts @@ -8,10 +8,14 @@ export type QueryTransformer = ( selectionSet: SelectionSetNode, fragments: Record, delegationContext: DelegationContext, - transformationContext: Record, + transformationContext: Record ) => SelectionSetNode; -export type ResultTransformer = (result: any, delegationContext: DelegationContext, transformationContext: Record) => any; +export type ResultTransformer = ( + result: any, + delegationContext: DelegationContext, + transformationContext: Record +) => any; export type ErrorPathTransformer = (path: ReadonlyArray) => Array; @@ -59,7 +63,12 @@ export default class TransformQuery implements Transform { index++; if (index === pathLength) { - const selectionSet = this.queryTransformer(node.selectionSet, this.fragments, delegationContext, transformationContext); + const selectionSet = this.queryTransformer( + node.selectionSet, + this.fragments, + delegationContext, + transformationContext + ); return { ...node, @@ -87,12 +96,17 @@ export default class TransformQuery implements Transform { const data = this.transformData(originalResult.data, delegationContext, transformationContext); const errors = originalResult.errors; return { + ...originalResult, data, errors: errors != null ? this.transformErrors(errors) : undefined, }; } - private transformData(data: any, delegationContext: DelegationContext, transformationContext: Record): any { + private transformData( + data: any, + delegationContext: DelegationContext, + transformationContext: Record + ): any { const leafIndex = this.path.length - 1; let index = 0; let newData = data; diff --git a/packages/wrap/src/transforms/WrapQuery.ts b/packages/wrap/src/transforms/WrapQuery.ts index 04b7dbc84ba..3055910a0f9 100644 --- a/packages/wrap/src/transforms/WrapQuery.ts +++ b/packages/wrap/src/transforms/WrapQuery.ts @@ -77,6 +77,7 @@ export default class WrapQuery implements Transform { } return { + ...originalResult, data: rootData, errors: originalResult.errors, }; From b844c3e11b3c205cfae2f402b151715621ae2ebc Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 1 Jun 2021 23:25:29 +0300 Subject: [PATCH 47/49] refactor batchDelegateToSchema ...to use DataLoader for the actual GraphQL results, rather than the external values This allows us to properly transduce streams. TODO: 1. We should now be able to add the stream directive for batchDelegateToSchema. 2. Refactor error parsing, now we are relying on the onLocatedError option, but this is no longer necessary. Note that additional tests for batched errors are now passing (see #2951) 3. Bring back createBatchDelegateFn, that may be useful 4. The Receiver is now created separately for each list item, which means that the initialPathDepth probably does not match? This will require a separate argument to the Receiver constructor for adjustment 5. Bring back valuesFromResults and fix documentation, we now operate on GraphQL results, so should be fewer pitfalls (see #2829) --- .../src/batchDelegateToSchema.ts | 81 +++++++- .../src/createBatchDelegateFn.ts | 29 --- packages/batch-delegate/src/getLoader.ts | 194 +++++++++++------- packages/batch-delegate/src/index.ts | 1 - packages/batch-delegate/src/types.ts | 8 - .../tests/withTransforms.test.ts | 3 +- packages/batch-execute/src/splitResult.ts | 31 ++- packages/delegate/src/Receiver.ts | 16 +- .../stitch/src/createMergedTypeResolver.ts | 8 +- packages/utils/src/splitAsyncIterator.ts | 28 +-- .../utils/tests/splitAsyncIterator.spec.ts | 8 +- 11 files changed, 259 insertions(+), 148 deletions(-) delete mode 100644 packages/batch-delegate/src/createBatchDelegateFn.ts diff --git a/packages/batch-delegate/src/batchDelegateToSchema.ts b/packages/batch-delegate/src/batchDelegateToSchema.ts index ee5057522b8..81e47bdd8dc 100644 --- a/packages/batch-delegate/src/batchDelegateToSchema.ts +++ b/packages/batch-delegate/src/batchDelegateToSchema.ts @@ -1,14 +1,89 @@ import { BatchDelegateOptions } from './types'; +import { AsyncExecutionResult, ExecutionResult, getNullableType, GraphQLList } from 'graphql'; + +import { + DelegationContext, + createRequestFromInfo, + externalValueFromResult, + getDelegationContext, + getDelegatingOperation, + Receiver, +} from '@graphql-tools/delegate'; + +import { isAsyncIterable, 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 loader = getLoader(options); - return Array.isArray(key) ? loader.loadMany(key) : loader.load(key); + + 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}'.`); + } + + const request = createRequestFromInfo({ + info, + operation, + fieldName, + selectionSet, + fieldNodes, + operationName, + }); + + const delegationContext = getDelegationContext({ + request, + onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)), + ...options, + operation, + fieldName, + returnType, + }); + + const loader = getLoader(options, request, delegationContext); + + if (Array.isArray(key)) { + const results = await loader.loadMany(key); + + return results.map(result => + onResult(result, { + ...delegationContext, + returnType: (getNullableType(delegationContext.returnType) as GraphQLList).ofType, + }) + ); + } + + const result = await loader.load(key); + return onResult(result, delegationContext); +} + +function onResult( + result: Error | ExecutionResult | AsyncIterableIterator, + delegationContext: DelegationContext +): any { + if (result instanceof Error) { + return result; + } + + if (isAsyncIterable(result)) { + const receiver = new Receiver(result, delegationContext); + return receiver.getInitialValue(); + } + + return externalValueFromResult(result, delegationContext); } diff --git a/packages/batch-delegate/src/createBatchDelegateFn.ts b/packages/batch-delegate/src/createBatchDelegateFn.ts deleted file mode 100644 index bd41f3a5d7b..00000000000 --- a/packages/batch-delegate/src/createBatchDelegateFn.ts +++ /dev/null @@ -1,29 +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, -): BatchDelegateFn { - return typeof optionsOrArgsFromKeys === 'function' - ? createBatchDelegateFnImpl({ - argsFromKeys: optionsOrArgsFromKeys, - lazyOptionsFn, - dataLoaderOptions, - }) - : 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 536efdd8360..f27ce64ceab 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -1,19 +1,17 @@ -import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode } from 'graphql'; +import { GraphQLSchema, FieldNode } from 'graphql'; import DataLoader from 'dataloader'; +import { SubschemaConfig, Transformer, getExecutor, validateRequest, DelegationContext } from '@graphql-tools/delegate'; import { - SubschemaConfig, - Transformer, - createRequestFromInfo, - getDelegationContext, - getDelegatingOperation, - getExecutor, - validateRequest, - Receiver, - externalValueFromResult, -} from '@graphql-tools/delegate'; -import { isAsyncIterable, relocatedError } from '@graphql-tools/utils'; + AsyncExecutionResult, + ExecutionPatchResult, + ExecutionResult, + isAsyncIterable, + mapAsyncIterator, + Request, + splitAsyncIterator, +} from '@graphql-tools/utils'; import { BatchDelegateOptions } from './types'; @@ -22,48 +20,21 @@ const cache1: WeakMap< WeakMap>> > = new WeakMap(); -function createBatchFn(options: BatchDelegateOptions) { +function createBatchFn(options: BatchDelegateOptions, request: Request, delegationContext: DelegationContext) { const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray) => ({ ids: keys })); - const { lazyOptionsFn } = options; - return async (keys: ReadonlyArray) => { - const { - context, - info, - operationName, - operation = getDelegatingOperation(info.parentType, info.schema), - fieldName = info.fieldName, - returnType = new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), - selectionSet, - fieldNodes, - binding, - skipValidation, - } = options; - - if (operation !== 'query' && operation !== 'mutation') { - throw new Error(`Batch delegation not possible for operation '${operation}'.`); - } + const { binding, skipValidation } = options; - const request = createRequestFromInfo({ - info, - operation, - fieldName, - selectionSet, - fieldNodes, - operationName, - }); + const { fieldName, context, info } = delegationContext; - const delegationContext = getDelegationContext({ - request, - args: argsFromKeys(keys), - onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)), - ...(lazyOptionsFn == null ? options : lazyOptionsFn(options)), - operation, - fieldName, - returnType, - }); - - const transformer = new Transformer(delegationContext, binding); + return async (keys: ReadonlyArray) => { + const transformer = new Transformer( + { + ...delegationContext, + args: argsFromKeys(keys), + }, + binding + ); const processedRequest = transformer.transformRequest(request); @@ -79,40 +50,38 @@ function createBatchFn(options: BatchDelegateOptions) { info, }); + const numKeys = keys.length; if (isAsyncIterable(batchResult)) { - // TODO: split the asyncIterable and make a new receiver from each of them, return the Receiver instead of the - // initial value, so that the correct info can be used to instantiate the Receiver - const receiver = new Receiver(batchResult, delegationContext, executionResult => - transformer.transformResult(executionResult) - ); - - const batchValue = await receiver.getInitialValue(); - - return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); + const mappedBatchResult = mapAsyncIterator(batchResult, result => transformer.transformResult(result)); + return splitAsyncIterator(mappedBatchResult, numKeys, result => splitAsyncResult(result, fieldName)); } - // TODO: split the batchedResult and return the result instead of the value, so the correct info - // can be used to instantiate the value - const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext); - - return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue); + return splitResult(transformer.transformResult(batchResult), fieldName, numKeys); }; } -export function getLoader(options: BatchDelegateOptions): DataLoader { +export function getLoader( + options: BatchDelegateOptions, + request: Request, + delegationContext: DelegationContext +): DataLoader, C> { const fieldName = options.fieldName ?? options.info.fieldName; - let cache2: WeakMap>> = cache1.get( - options.info.fieldNodes - ); + let cache2: WeakMap< + GraphQLSchema | SubschemaConfig, + Record, C>> + > = cache1.get(options.info.fieldNodes); if (cache2 === undefined) { cache2 = new WeakMap(); cache1.set(options.info.fieldNodes, cache2); const loaders = Object.create(null); cache2.set(options.schema, loaders); - const batchFn = createBatchFn(options); - const loader = new DataLoader(keys => batchFn(keys), options.dataLoaderOptions); + const batchFn = createBatchFn(options, request, delegationContext); + const loader = new DataLoader, C>( + keys => batchFn(keys), + options.dataLoaderOptions + ); loaders[fieldName] = loader; return loader; } @@ -122,8 +91,11 @@ export function getLoader(options: BatchDelegateOptions if (loaders === undefined) { loaders = Object.create(null); cache2.set(options.schema, loaders); - const batchFn = createBatchFn(options); - const loader = new DataLoader(keys => batchFn(keys), options.dataLoaderOptions); + const batchFn = createBatchFn(options, request, delegationContext); + const loader = new DataLoader, C>( + keys => batchFn(keys), + options.dataLoaderOptions + ); loaders[fieldName] = loader; return loader; } @@ -131,10 +103,84 @@ export function getLoader(options: BatchDelegateOptions let loader = loaders[fieldName]; if (loader === undefined) { - const batchFn = createBatchFn(options); - loader = new DataLoader(keys => batchFn(keys), options.dataLoaderOptions); + const batchFn = createBatchFn(options, request, delegationContext); + loader = new DataLoader, C>( + keys => batchFn(keys), + options.dataLoaderOptions + ); loaders[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, + })); +} + +function splitAsyncResult(result: AsyncExecutionResult, fieldName: string): [[number, AsyncExecutionResult]] { + const { data, errors, path } = result as ExecutionPatchResult; + + if (path === undefined || path.length === 0) { + const fieldData = data?.[fieldName]; + if (fieldData !== undefined) { + return fieldData.map((value: any, index: number) => [ + index, + { + data: { + [fieldName]: value, + }, + errors, + }, + ]); + } + } else if (path[0] === fieldName) { + const index = path[1] as number; + + if (path.length === 2) { + return [ + [ + index, + { + ...result, + data: { + [fieldName]: data, + }, + errors, + }, + ], + ]; + } + + const newPath = [fieldName, ...path.slice(2)]; + return [ + [ + index, + { + ...result, + data, + errors, + path: newPath, + }, + ], + ]; + } + + return [[undefined, result]]; +} 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 2e8c96e3e83..2cd886e227f 100644 --- a/packages/batch-delegate/src/types.ts +++ b/packages/batch-delegate/src/types.ts @@ -15,12 +15,4 @@ export interface BatchDelegateOptions, K = any, V dataLoaderOptions?: DataLoader.Options; key: K; argsFromKeys?: (keys: ReadonlyArray) => Record; - lazyOptionsFn?: BatchDelegateOptionsFn; -} - -export interface CreateBatchDelegateFnOptions, K = any, V = any, C = K> - extends Partial, 'args' | 'info'>> { - dataLoaderOptions?: DataLoader.Options; - argsFromKeys?: (keys: ReadonlyArray) => Record; - lazyOptionsFn?: (batchDelegateOptions: BatchDelegateOptions) => IDelegateToSchemaOptions; } diff --git a/packages/batch-delegate/tests/withTransforms.test.ts b/packages/batch-delegate/tests/withTransforms.test.ts index 17189edf9d0..3b6c2752fca 100644 --- a/packages/batch-delegate/tests/withTransforms.test.ts +++ b/packages/batch-delegate/tests/withTransforms.test.ts @@ -1,4 +1,4 @@ -import { graphql, GraphQLList, Kind } from 'graphql'; +import { graphql, Kind } 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'))) }), }, }, diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index 1b5bc9a8111..c62222cd5cc 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -2,7 +2,13 @@ import { ExecutionResult, GraphQLError } from 'graphql'; -import { AsyncExecutionResult, ExecutionPatchResult, isAsyncIterable, relocatedError, splitAsyncIterator } from '@graphql-tools/utils'; +import { + AsyncExecutionResult, + ExecutionPatchResult, + isAsyncIterable, + relocatedError, + splitAsyncIterator, +} from '@graphql-tools/utils'; import { ValueOrPromise } from 'value-or-promise'; @@ -19,7 +25,9 @@ export function splitResult( | AsyncIterableIterator | Promise> > { - const result = new ValueOrPromise(() => mergedResult).then(r => splitExecutionResultOrAsyncIterableIterator(r, numResults)); + const result = new ValueOrPromise(() => mergedResult).then(r => + splitExecutionResultOrAsyncIterableIterator(r, numResults) + ); const splitResults: Array< | ExecutionResult @@ -27,9 +35,12 @@ export function splitResult( | Promise> > = []; for (let i = 0; i < numResults; i++) { - splitResults.push(result.then(r => r[i]).resolve() as ExecutionResult - | AsyncIterableIterator - | Promise>); + splitResults.push( + result.then(r => r[i]).resolve() as + | ExecutionResult + | AsyncIterableIterator + | Promise> + ); } return splitResults; @@ -40,13 +51,15 @@ export function splitExecutionResultOrAsyncIterableIterator( numResults: number ): Array> { if (isAsyncIterable(mergedResult)) { - return splitAsyncIterator(mergedResult, numResults, originalResult => splitExecutionPatchResult(originalResult as ExecutionPatchResult)); + return splitAsyncIterator(mergedResult, numResults, originalResult => + splitExecutionPatchResult(originalResult as ExecutionPatchResult) + ); } return splitExecutionResult(mergedResult, numResults); } -function splitExecutionPatchResult(originalResult: ExecutionPatchResult): [number, ExecutionPatchResult] { +function splitExecutionPatchResult(originalResult: ExecutionPatchResult): [[number, ExecutionPatchResult]] { const path = originalResult.path; if (path && path.length) { const { index, originalKey } = parseKey(path[0] as string); @@ -76,7 +89,7 @@ function splitExecutionPatchResult(originalResult: ExecutionPatchResult): [numbe newResult.errors = newErrors; } - return [index, newResult]; + return [[index, newResult]]; } let resultIndex: number; @@ -112,7 +125,7 @@ function splitExecutionPatchResult(originalResult: ExecutionPatchResult): [numbe newResult.errors = newErrors; } - return [resultIndex, newResult] + return [[resultIndex, newResult]]; } /** diff --git a/packages/delegate/src/Receiver.ts b/packages/delegate/src/Receiver.ts index e3d67c5b274..d479467897f 100644 --- a/packages/delegate/src/Receiver.ts +++ b/packages/delegate/src/Receiver.ts @@ -119,6 +119,15 @@ export class Receiver { ): Promise> { const parentPath = path.slice(); const responseKey = parentPath.pop() as string; + + const indices: Array = []; + let lastSegment = parentPath.length - 1; + while (typeof parentPath[lastSegment] === 'number') { + const index = parentPath.pop() as number; + indices.push(index); + lastSegment--; + } + const parentKey = parentPath.join('.'); const combinedInfo: GraphQLResolveInfo = { @@ -132,7 +141,12 @@ export class Receiver { throw new Error(`Parent with key "${parentKey}" not available.`); } - const data = parent.data[responseKey]; + let data = parent.data; + for (const index of indices) { + data = data[index]; + } + data = data[responseKey]; + if (data !== undefined) { const newResult = { data, unpathedErrors: parent.unpathedErrors }; this._update(combinedInfo, newResult, pathKey); diff --git a/packages/stitch/src/createMergedTypeResolver.ts b/packages/stitch/src/createMergedTypeResolver.ts index 05a8fabf9b4..278d52d0d25 100644 --- a/packages/stitch/src/createMergedTypeResolver.ts +++ b/packages/stitch/src/createMergedTypeResolver.ts @@ -1,4 +1,4 @@ -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'; @@ -11,9 +11,9 @@ export function createMergedTypeResolver(mergedTypeResolverOptions: MergedTypeRe 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, selectionSet, diff --git a/packages/utils/src/splitAsyncIterator.ts b/packages/utils/src/splitAsyncIterator.ts index 709bb9d90e8..8d11a7d66ee 100644 --- a/packages/utils/src/splitAsyncIterator.ts +++ b/packages/utils/src/splitAsyncIterator.ts @@ -4,7 +4,7 @@ import { Repeater } from '@repeaterjs/repeater'; -type Splitter = (item: T) => [number | undefined, T]; +type Splitter = (item: T) => [[number | undefined, T]]; export function splitAsyncIterator(iterator: AsyncIterator, n: number, splitter: Splitter) { const returner = iterator.return?.bind(iterator) ?? (() => true); @@ -62,21 +62,23 @@ async function next( const iterationCandidate = await iterator.next(); - let tee = true; const value = iterationCandidate.value; if (value !== undefined) { - const [iterationIndex, newValue] = splitter(value); - - if (iterationIndex !== undefined) { - buffers[iterationIndex].push({ - ...iterationCandidate, - value: newValue, - }); - tee = false; + const assignments = splitter(value); + + for (const [iterationIndex, newValue] of assignments) { + if (iterationIndex !== undefined) { + buffers[iterationIndex].push({ + ...iterationCandidate, + value: newValue, + }); + } else { + for (const b of buffers) { + b.push(iterationCandidate); + } + } } - } - - if (tee) { + } else { for (const b of buffers) { b.push(iterationCandidate); } diff --git a/packages/utils/tests/splitAsyncIterator.spec.ts b/packages/utils/tests/splitAsyncIterator.spec.ts index a92a77da091..4274720b783 100644 --- a/packages/utils/tests/splitAsyncIterator.spec.ts +++ b/packages/utils/tests/splitAsyncIterator.spec.ts @@ -9,7 +9,7 @@ describe('splitAsyncIterator', () => { } }(); - const [one, two] = splitAsyncIterator(gen3, 2, (x) => [0, x + 5]); + const [one, two] = splitAsyncIterator(gen3, 2, (x) => [[0, x + 5]]); let results = []; for await (const result of one) { @@ -31,7 +31,7 @@ describe('splitAsyncIterator', () => { } }(); - const [one, two] = splitAsyncIterator(gen3, 2, (x) => [0, x + 5]); + const [one, two] = splitAsyncIterator(gen3, 2, (x) => [[0, x + 5]]); const oneResults = []; const twoResults = []; @@ -55,7 +55,7 @@ describe('splitAsyncIterator with mapAsyncIterator', () => { }(); const mappedGen3 = mapAsyncIterator(gen3, value => value); - const [one, two] = splitAsyncIterator(mappedGen3, 2, (x) => [0, x + 5]); + const [one, two] = splitAsyncIterator(mappedGen3, 2, (x) => [[0, x + 5]]); let results = []; for await (const result of one) { @@ -78,7 +78,7 @@ describe('splitAsyncIterator with mapAsyncIterator', () => { }(); const mappedGen3 = mapAsyncIterator(gen3, value => value); - const [one, two] = splitAsyncIterator(mappedGen3, 2, (x) => [0, x + 5]); + const [one, two] = splitAsyncIterator(mappedGen3, 2, (x) => [[0, x + 5]]); const oneResults = []; const twoResults = []; From 8aac856d2998a02f315491e6a56151b3db7afcca Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 1 Jun 2021 23:37:14 +0300 Subject: [PATCH 48/49] fix types for now try to avoid breaking changes (in the future?) --- packages/delegate/src/Subschema.ts | 10 +++------- packages/delegate/src/types.ts | 10 ++++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/delegate/src/Subschema.ts b/packages/delegate/src/Subschema.ts index 01754e00e43..5c77d3d9047 100644 --- a/packages/delegate/src/Subschema.ts +++ b/packages/delegate/src/Subschema.ts @@ -9,13 +9,9 @@ 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; public rootValue?: Record; @@ -28,7 +24,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/types.ts b/packages/delegate/src/types.ts index fc73d45323e..5287e6b901f 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -152,7 +152,7 @@ export interface SubschemaConfig; transforms?: Array; - merge?: Record>; + merge?: Record>; rootValue?: Record; executor?: Executor; subscriber?: Subscriber; @@ -160,22 +160,20 @@ export interface SubschemaConfig; } -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; From 3a079fc30c01ee3984e7715eb5a4937927842d31 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 2 Jun 2021 00:36:36 +0300 Subject: [PATCH 49/49] add polyfill back --- packages/delegate/src/mergeDataAndErrors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/delegate/src/mergeDataAndErrors.ts b/packages/delegate/src/mergeDataAndErrors.ts index 8a03639f72d..aefb34dfd77 100644 --- a/packages/delegate/src/mergeDataAndErrors.ts +++ b/packages/delegate/src/mergeDataAndErrors.ts @@ -1,3 +1,5 @@ +import AggregateError from '@ardatan/aggregate-error'; + import { GraphQLError, locatedError } from 'graphql'; import { relocatedError } from '@graphql-tools/utils';