Skip to content

Commit

Permalink
add batchDelegate package
Browse files Browse the repository at this point in the history
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:
#1710
  • Loading branch information
yaacovCR committed Jul 8, 2020
1 parent 70443c8 commit 4cc800a
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 25 deletions.
34 changes: 34 additions & 0 deletions packages/batchDelegate/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
51 changes: 51 additions & 0 deletions packages/batchDelegate/src/createBatchDelegateFn.ts
Original file line number Diff line number Diff line change
@@ -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<K = any, V = any, C = K>(
argFn: (args: ReadonlyArray<K>) => Record<string, any>,
batchDelegateOptionsFn: BatchDelegateOptionsFn,
dataLoaderOptions?: DataLoader.Options<K, V, C>
): BatchDelegateFn<K> {
let cache: WeakMap<ReadonlyArray<FieldNode>, DataLoader<K, V, C>>;

function createBatchFn(options: BatchDelegateOptions) {
return async (keys: ReadonlyArray<K>) => {
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<K, V, C>(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<K, V, C>(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);
};
}
3 changes: 3 additions & 0 deletions packages/batchDelegate/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { createBatchDelegateFn } from './createBatchDelegateFn';

export * from './types';
14 changes: 14 additions & 0 deletions packages/batchDelegate/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IDelegateToSchemaOptions } from '@graphql-tools/delegate';

export type BatchDelegateFn<TContext = Record<string, any>, K = any> = (
batchDelegateOptions: BatchDelegateOptions<TContext, K>
) => any;

export type BatchDelegateOptionsFn<TContext = Record<string, any>, K = any> = (
batchDelegateOptions: BatchDelegateOptions<TContext, K>
) => IDelegateToSchemaOptions<TContext>;

export interface BatchDelegateOptions<TContext = Record<string, any>, K = any>
extends Omit<IDelegateToSchemaOptions<TContext>, 'args'> {
key: K;
}
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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!
Expand All @@ -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)),
},
},
});
Expand Down Expand Up @@ -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!
Expand All @@ -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)),
}
},
});
Expand Down Expand Up @@ -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: {
Expand All @@ -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 })),
},
}
});
Expand All @@ -212,8 +220,8 @@ describe('merging using type merging', () => {
schema: accountsSchema,
merge: {
User: {
fieldName: '_userById',
selectionSet: '{ id }',
fieldName: '_userById',
args: ({ id }) => ({ id })
}
}
Expand All @@ -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 }),
}
}
Expand All @@ -232,8 +240,8 @@ describe('merging using type merging', () => {
schema: productsSchema,
merge: {
Product: {
fieldName: '_productByUpc',
selectionSet: '{ upc }',
fieldName: '_productByUpc',
args: ({ upc }) => ({ upc }),
}
}
Expand All @@ -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 }),
},
}
Expand Down
3 changes: 2 additions & 1 deletion packages/delegate/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ export interface SubschemaConfig {
export interface MergedTypeConfig {
selectionSet?: string;
fieldName?: string;
args?: (originalResult: any) => Record<string, any>;
args?: (source: any) => Record<string, any>;
key?: (originalResult: any) => any;
resolve?: MergedTypeResolver;
}

Expand Down
49 changes: 37 additions & 12 deletions packages/stitch/src/stitchingInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 4cc800a

Please sign in to comment.