Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce batch delegation #1735

Merged
merged 5 commits into from Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/batch-delegate/package.json
@@ -0,0 +1,34 @@
{
"name": "@graphql-tools/batch-delegate",
"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/batch-delegate/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/batch-delegate/src/index.ts
@@ -0,0 +1,3 @@
export { createBatchDelegateFn } from './createBatchDelegateFn';

export * from './types';
14 changes: 14 additions & 0 deletions packages/batch-delegate/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
66 changes: 64 additions & 2 deletions packages/delegate/src/createMergedResolver.ts
@@ -1,8 +1,70 @@
import { IFieldResolver } from '@graphql-tools/utils';
import { GraphQLError } from 'graphql';

import { IFieldResolver, getErrors, setErrors, relocatedError, ERROR_SYMBOL } from '@graphql-tools/utils';

import { OBJECT_SUBSCHEMA_SYMBOL } from './symbols';

import { getSubschema, setObjectSubschema } from './Subschema';

import { unwrapResult, dehoistResult } from './proxiedResult';
import { defaultMergedResolver } from './defaultMergedResolver';

import { handleNull } from './results/handleNull';

function unwrapResult(parent: any, path: Array<string>): any {
let newParent: any = parent;
const pathLength = path.length;
for (let i = 0; i < pathLength; i++) {
const responseKey = path[i];
const errors = getErrors(newParent, responseKey);
const subschema = getSubschema(newParent, responseKey);

const object = newParent[responseKey];
if (object == null) {
return handleNull(errors);
}

setErrors(
object,
errors.map(error => relocatedError(error, error.path != null ? error.path.slice(1) : undefined))
);
setObjectSubschema(object, subschema);

newParent = object;
}

return newParent;
}

function dehoistResult(parent: any, delimeter = '__gqltf__'): any {
const result = Object.create(null);

Object.keys(parent).forEach(alias => {
let obj = result;

const fieldNames = alias.split(delimeter);
const fieldName = fieldNames.pop();
fieldNames.forEach(key => {
obj = obj[key] = obj[key] || Object.create(null);
});
obj[fieldName] = parent[alias];
});

result[ERROR_SYMBOL] = parent[ERROR_SYMBOL].map((error: GraphQLError) => {
if (error.path != null) {
const path = error.path.slice();
const pathSegment = path.shift();
const expandedPathSegment: Array<string | number> = (pathSegment as string).split(delimeter);
return relocatedError(error, expandedPathSegment.concat(path));
}

return error;
});

result[OBJECT_SUBSCHEMA_SYMBOL] = parent[OBJECT_SUBSCHEMA_SYMBOL];

return result;
}

export function createMergedResolver({
fromPath,
dehoist,
Expand Down
89 changes: 0 additions & 89 deletions packages/delegate/src/proxiedResult.ts

This file was deleted.