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 59b5606
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 27 deletions.
34 changes: 34 additions & 0 deletions 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"
}
}
51 changes: 51 additions & 0 deletions 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<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
@@ -0,0 +1,3 @@
export { createBatchDelegateFn } from './createBatchDelegateFn';

export * from './types';
14 changes: 14 additions & 0 deletions packages/batchDelegate/src/types.ts
@@ -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;
}
@@ -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
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
3 changes: 2 additions & 1 deletion packages/graphql-tools/package.json
Expand Up @@ -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",
Expand All @@ -41,4 +42,4 @@
"@graphql-tools/utils": "6.0.12",
"@graphql-tools/wrap": "6.0.12"
}
}
}
1 change: 1 addition & 0 deletions 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';
Expand Down
3 changes: 2 additions & 1 deletion packages/stitch/package.json
Expand Up @@ -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",
Expand All @@ -32,4 +33,4 @@
"access": "public",
"directory": "dist"
}
}
}
49 changes: 37 additions & 12 deletions packages/stitch/src/stitchingInfo.ts
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 59b5606

Please sign in to comment.