From 59b5606b7d4a5e4839b6bd43d9c873b1999a3bd6 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 7 Jul 2020 22:01:57 -0400 Subject: [PATCH] add batchDelegate package Add key option within type merging config to enable batch loading for lists. This minimal version of batching uses cached dataLoaders to create a separate batch for each list rather than for every similar query within the resolution tree. This is because a new dataloader is created for every new info.fieldNodes object, which is memoized upstream by graphql-js within resolution of a given list to allow the loader to be used for items of that list. A future version could provide an option to batch by similar target fieldName/selectionSet, but this version may hit the sweet spot in terms of code complexity and batching behavior. see: https://github.com/ardatan/graphql-tools/issues/1710 --- packages/batchDelegate/package.json | 34 +++++++++++++ .../src/createBatchDelegateFn.ts | 51 +++++++++++++++++++ packages/batchDelegate/src/index.ts | 3 ++ packages/batchDelegate/src/types.ts | 14 +++++ .../tests/typeMerging.example.test.ts | 33 +++++++----- packages/delegate/src/types.ts | 3 +- packages/graphql-tools/package.json | 3 +- packages/graphql-tools/src/index.ts | 1 + packages/stitch/package.json | 3 +- packages/stitch/src/stitchingInfo.ts | 49 +++++++++++++----- 10 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 packages/batchDelegate/package.json create mode 100644 packages/batchDelegate/src/createBatchDelegateFn.ts create mode 100644 packages/batchDelegate/src/index.ts create mode 100644 packages/batchDelegate/src/types.ts rename packages/{stitch => batchDelegate}/tests/typeMerging.example.test.ts (90%) diff --git a/packages/batchDelegate/package.json b/packages/batchDelegate/package.json new file mode 100644 index 00000000000..b71f41dba7f --- /dev/null +++ b/packages/batchDelegate/package.json @@ -0,0 +1,34 @@ +{ + "name": "@graphql-tools/batchDelegate", + "version": "6.0.12", + "description": "A set of utils for faster development of GraphQL tools", + "repository": "git@github.com:ardatan/graphql-tools.git", + "license": "MIT", + "sideEffects": false, + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "dependencies": { + "@graphql-tools/delegate": "6.0.12", + "dataloader": "2.0.0", + "tslib": "~2.0.0" + }, + "devDependencies": { + "@graphql-tools/schema": "6.0.12", + "@graphql-tools/stitch": "6.0.12", + "@graphql-tools/utils": "6.0.12" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + } +} diff --git a/packages/batchDelegate/src/createBatchDelegateFn.ts b/packages/batchDelegate/src/createBatchDelegateFn.ts new file mode 100644 index 00000000000..cf8887ff367 --- /dev/null +++ b/packages/batchDelegate/src/createBatchDelegateFn.ts @@ -0,0 +1,51 @@ +import { FieldNode, getNamedType, GraphQLOutputType, GraphQLList } from 'graphql'; + +import DataLoader from 'dataloader'; + +import { delegateToSchema } from '@graphql-tools/delegate'; + +import { BatchDelegateOptionsFn, BatchDelegateFn, BatchDelegateOptions } from './types'; + +export function createBatchDelegateFn( + argFn: (args: ReadonlyArray) => Record, + batchDelegateOptionsFn: BatchDelegateOptionsFn, + dataLoaderOptions?: DataLoader.Options +): BatchDelegateFn { + let cache: WeakMap, DataLoader>; + + function createBatchFn(options: BatchDelegateOptions) { + return async (keys: ReadonlyArray) => { + const results = await delegateToSchema({ + returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), + args: argFn(keys), + ...batchDelegateOptionsFn(options), + }); + return Array.isArray(results) ? results : keys.map(() => results); + }; + } + + function getLoader(options: BatchDelegateOptions) { + if (!cache) { + cache = new WeakMap(); + const batchFn = createBatchFn(options); + const newValue = new DataLoader(keys => batchFn(keys), dataLoaderOptions); + cache.set(options.info.fieldNodes, newValue); + return newValue; + } + + const cachedValue = cache.get(options.info.fieldNodes); + if (cachedValue === undefined) { + const batchFn = createBatchFn(options); + const newValue = new DataLoader(keys => batchFn(keys), dataLoaderOptions); + cache.set(options.info.fieldNodes, newValue); + return newValue; + } + + return cachedValue; + } + + return options => { + const loader = getLoader(options); + return loader.load(options.key); + }; +} diff --git a/packages/batchDelegate/src/index.ts b/packages/batchDelegate/src/index.ts new file mode 100644 index 00000000000..9b6e426afb3 --- /dev/null +++ b/packages/batchDelegate/src/index.ts @@ -0,0 +1,3 @@ +export { createBatchDelegateFn } from './createBatchDelegateFn'; + +export * from './types'; diff --git a/packages/batchDelegate/src/types.ts b/packages/batchDelegate/src/types.ts new file mode 100644 index 00000000000..b935a6369b4 --- /dev/null +++ b/packages/batchDelegate/src/types.ts @@ -0,0 +1,14 @@ +import { IDelegateToSchemaOptions } from '@graphql-tools/delegate'; + +export type BatchDelegateFn, K = any> = ( + batchDelegateOptions: BatchDelegateOptions +) => any; + +export type BatchDelegateOptionsFn, K = any> = ( + batchDelegateOptions: BatchDelegateOptions +) => IDelegateToSchemaOptions; + +export interface BatchDelegateOptions, K = any> + extends Omit, 'args'> { + key: K; +} diff --git a/packages/stitch/tests/typeMerging.example.test.ts b/packages/batchDelegate/tests/typeMerging.example.test.ts similarity index 90% rename from packages/stitch/tests/typeMerging.example.test.ts rename to packages/batchDelegate/tests/typeMerging.example.test.ts index 3c16786464d..91603dadf77 100644 --- a/packages/stitch/tests/typeMerging.example.test.ts +++ b/packages/batchDelegate/tests/typeMerging.example.test.ts @@ -1,16 +1,16 @@ -// Conversion of Apollo Federation demo from https://github.com/apollographql/federation-demo. -// See: https://github.com/ardatan/graphql-tools/issues/1697 +// Conversion of Apollo Federation demo +// Compare: https://github.com/apollographql/federation-demo +// See also: +// https://github.com/ardatan/graphql-tools/issues/1697 +// https://github.com/ardatan/graphql-tools/issues/1710 import { graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; - import { ExecutionResult } from '@graphql-tools/utils'; - -import { stitchSchemas } from '../src/stitchSchemas'; +import { stitchSchemas } from '@graphql-tools/stitch'; describe('merging using type merging', () => { - const users = [ { id: '1', @@ -31,6 +31,7 @@ describe('merging using type merging', () => { type Query { me: User _userById(id: ID!): User + _usersById(ids: [ID!]!): [User] } type User { id: ID! @@ -42,6 +43,7 @@ describe('merging using type merging', () => { Query: { me: () => users[0], _userById: (_root, { id }) => users.find(user => user.id === id), + _usersById: (_root, { ids }) => ids.map((id: any) => users.find(user => user.id === id)), }, }, }); @@ -111,6 +113,7 @@ describe('merging using type merging', () => { type Query { topProducts(first: Int = 5): [Product] _productByUpc(upc: String!): Product + _productsByUpc(upcs: [String!]!): [Product] } type Product { upc: String! @@ -123,6 +126,7 @@ describe('merging using type merging', () => { Query: { topProducts: (_root, args) => products.slice(0, args.first), _productByUpc: (_root, { upc }) => products.find(product => product.upc === upc), + _productsByUpc: (_root, { upcs }) => upcs.map((upc: any) => products.find(product => product.upc === upc)), } }, }); @@ -179,8 +183,10 @@ describe('merging using type merging', () => { } type Query { _userById(id: ID!): User + _usersById(ids: [ID!]!): [User] _reviewById(id: ID!): Review _productByUpc(upc: String!): Product + _productsByUpc(upcs: [String!]!): [Product] } `, resolvers: { @@ -201,7 +207,9 @@ describe('merging using type merging', () => { Query: { _reviewById: (_root, { id }) => reviews.find(review => review.id === id), _userById: (_root, { id }) => ({ id }), + _usersById: (_root, { ids }) => ids.map((id: string) => ({ id })), _productByUpc: (_, { upc }) => ({ upc }), + _productsByUpc: (_, { upcs }) => upcs.map((upc: string) => ({ upc })), }, } }); @@ -212,8 +220,8 @@ describe('merging using type merging', () => { schema: accountsSchema, merge: { User: { - fieldName: '_userById', selectionSet: '{ id }', + fieldName: '_userById', args: ({ id }) => ({ id }) } } @@ -222,8 +230,8 @@ describe('merging using type merging', () => { schema: inventorySchema, merge: { Product: { - fieldName: '_productByUpc', selectionSet: '{ upc weight price }', + fieldName: '_productByUpc', args: ({ upc, weight, price }) => ({ upc, weight, price }), } } @@ -232,8 +240,8 @@ describe('merging using type merging', () => { schema: productsSchema, merge: { Product: { - fieldName: '_productByUpc', selectionSet: '{ upc }', + fieldName: '_productByUpc', args: ({ upc }) => ({ upc }), } } @@ -242,13 +250,14 @@ describe('merging using type merging', () => { schema: reviewsSchema, merge: { User: { - fieldName: '_userById', selectionSet: '{ id }', - args: ({ id }) => ({ id }), + fieldName: '_usersById', + args: (ids) => ({ ids }), + key: ({ id }) => id, }, Product: { - fieldName: '_productByUpc', selectionSet: '{ upc }', + fieldName: '_productByUpc', args: ({ upc }) => ({ upc }), }, } diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 190d4234420..ccdc4e64f28 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -135,7 +135,8 @@ export interface SubschemaConfig { export interface MergedTypeConfig { selectionSet?: string; fieldName?: string; - args?: (originalResult: any) => Record; + args?: (source: any) => Record; + key?: (originalResult: any) => any; resolve?: MergedTypeResolver; } diff --git a/packages/graphql-tools/package.json b/packages/graphql-tools/package.json index 62797c86396..ccad40c7d96 100644 --- a/packages/graphql-tools/package.json +++ b/packages/graphql-tools/package.json @@ -19,6 +19,7 @@ "directory": "dist" }, "dependencies": { + "@graphql-tools/batchDelegate": "6.0.12", "@graphql-tools/delegate": "6.0.12", "@graphql-tools/graphql-tag-pluck": "6.0.12", "@graphql-tools/import": "6.0.12", @@ -41,4 +42,4 @@ "@graphql-tools/utils": "6.0.12", "@graphql-tools/wrap": "6.0.12" } -} \ No newline at end of file +} diff --git a/packages/graphql-tools/src/index.ts b/packages/graphql-tools/src/index.ts index 3e91ade0a88..12dcfdb2755 100644 --- a/packages/graphql-tools/src/index.ts +++ b/packages/graphql-tools/src/index.ts @@ -1,3 +1,4 @@ +export * from '@graphql-tools/batchDelegate'; export * from '@graphql-tools/delegate'; export * from '@graphql-tools/graphql-tag-pluck'; export * from '@graphql-tools/import'; diff --git a/packages/stitch/package.json b/packages/stitch/package.json index 8b657e33584..f0ed079aee3 100644 --- a/packages/stitch/package.json +++ b/packages/stitch/package.json @@ -21,6 +21,7 @@ "dataloader": "2.0.0" }, "dependencies": { + "@graphql-tools/batchDelegate": "6.0.12", "@graphql-tools/delegate": "6.0.12", "@graphql-tools/merge": "6.0.12", "@graphql-tools/schema": "6.0.12", @@ -32,4 +33,4 @@ "access": "public", "directory": "dist" } -} \ No newline at end of file +} diff --git a/packages/stitch/src/stitchingInfo.ts b/packages/stitch/src/stitchingInfo.ts index 40f741e3cb1..9a166e05cfd 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -21,6 +21,7 @@ import { } from '@graphql-tools/utils'; import { delegateToSchema, isSubschemaConfig, SubschemaConfig } from '@graphql-tools/delegate'; +import { createBatchDelegateFn } from '@graphql-tools/batchDelegate'; import { MergeTypeCandidate, MergedTypeInfo, StitchingInfo, MergeTypeFilter } from './types'; @@ -103,18 +104,42 @@ function createMergedTypes( } if (!mergedTypeConfig.resolve) { - mergedTypeConfig.resolve = (originalResult, context, info, subschema, selectionSet) => - delegateToSchema({ - schema: subschema, - operation: 'query', - fieldName: mergedTypeConfig.fieldName, - returnType: getNamedType(info.returnType) as GraphQLOutputType, - args: mergedTypeConfig.args(originalResult), - selectionSet, - context, - info, - skipTypeMerging: true, - }); + if (mergedTypeConfig.key != null) { + const batchDelegateToSubschema = createBatchDelegateFn( + mergedTypeConfig.args, + ({ schema, selectionSet, context, info }) => ({ + schema, + operation: 'query', + fieldName: mergedTypeConfig.fieldName, + selectionSet, + context, + info, + skipTypeMerging: true, + }) + ); + + mergedTypeConfig.resolve = (originalResult, context, info, subschema, selectionSet) => + batchDelegateToSubschema({ + key: mergedTypeConfig.key(originalResult), + schema: subschema, + context, + info, + selectionSet, + }); + } else { + mergedTypeConfig.resolve = (originalResult, context, info, subschema, selectionSet) => + delegateToSchema({ + schema: subschema, + operation: 'query', + fieldName: mergedTypeConfig.fieldName, + returnType: getNamedType(info.returnType) as GraphQLOutputType, + args: mergedTypeConfig.args(originalResult), + selectionSet, + context, + info, + skipTypeMerging: true, + }); + } } subschemas.push(subschemaConfig);