Skip to content

Commit

Permalink
use lazy query planner
Browse files Browse the repository at this point in the history
refactor externalObject internals into separate file

add tests from #2951

and fix them by reworking batch delegation

buildDelegationPlan can be used to calculate the rounds of delegation necessary to completely merge an object given the stitching metadata stored within the schema and a given set of fieldNodes

TODO: change buildDelegationPlan function to method on MergedTypeInfo as class? move back to stitch package?
  • Loading branch information
yaacovCR committed Sep 25, 2021
1 parent 14a8c22 commit 1daaa4a
Show file tree
Hide file tree
Showing 38 changed files with 1,482 additions and 905 deletions.
10 changes: 10 additions & 0 deletions .changeset/four-ways-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphql-tools/batch-delegate': major
'@graphql-tools/delegate': major
'@graphql-tools/stitch': major
'@graphql-tools/stitching-directives': major
'@graphql-tools/utils': major
'@graphql-tools/wrap': major
---

use lazy query planner
50 changes: 48 additions & 2 deletions packages/batch-delegate/src/batchDelegateToSchema.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
import { BatchDelegateOptions } from './types';

import { getNullableType, GraphQLError, GraphQLList } from 'graphql';

import { externalValueFromResult } from '@graphql-tools/delegate';
import { relocatedError } from '@graphql-tools/utils';

import { getLoader } from './getLoader';

export function batchDelegateToSchema<TContext = any>(options: BatchDelegateOptions<TContext>): any {
export async function batchDelegateToSchema<TContext = any>(options: BatchDelegateOptions<TContext>): Promise<any> {
const key = options.key;
if (key == null) {
return null;
} else if (Array.isArray(key) && !key.length) {
return [];
}

const {
schema,
info,
fieldName = info.fieldName,
returnType = info.returnType,
context,
onLocatedError = (originalError: GraphQLError) =>
relocatedError(originalError, originalError.path ? originalError.path.slice(1) : []),
} = options;

const loader = getLoader(options);
return Array.isArray(key) ? loader.loadMany(key) : loader.load(key);

if (Array.isArray(key)) {
const results = await loader.loadMany(key);

return results.map(result =>
result instanceof Error
? result
: externalValueFromResult({
result,
schema,
info,
context,
fieldName,
returnType: (getNullableType(returnType) as GraphQLList<any>).ofType,
onLocatedError,
})
);
}

const result = await loader.load(key);
return result instanceof Error
? result
: externalValueFromResult({
result,
schema,
info,
context,
fieldName,
returnType,
onLocatedError,
});
}
31 changes: 0 additions & 31 deletions packages/batch-delegate/src/createBatchDelegateFn.ts

This file was deleted.

133 changes: 97 additions & 36 deletions packages/batch-delegate/src/getLoader.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,81 @@
import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode } from 'graphql';
import { GraphQLSchema, FieldNode } from 'graphql';

import DataLoader from 'dataloader';

import { delegateToSchema, SubschemaConfig } from '@graphql-tools/delegate';
import { memoize2, relocatedError } from '@graphql-tools/utils';
import {
SubschemaConfig,
Transformer,
validateRequest,
getExecutor,
getDelegatingOperation,
createRequestFromInfo,
getDelegationContext,
} from '@graphql-tools/delegate';
import { ExecutionResult, memoize2 } from '@graphql-tools/utils';

import { BatchDelegateOptions } from './types';

function createBatchFn<K = any>(options: BatchDelegateOptions) {
function createBatchFn<K = any>(
options: BatchDelegateOptions
): (keys: ReadonlyArray<K>) => Promise<Array<ExecutionResult<Record<string, any>>>> {
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,
...options,
operation,
fieldName,
returnType,
});

const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray<K>) => ({ ids: keys }));
const fieldName = options.fieldName ?? options.info.fieldName;
const { valuesFromResults, lazyOptionsFn } = options;

const { validateRequest: shouldValidateRequest } = options;

return async function batchFn(keys: ReadonlyArray<K>) {
const results = await delegateToSchema({
returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType),
onLocatedError: originalError => {
if (originalError.path == null) {
return originalError;
}

const [pathFieldName, pathNumber] = originalError.path;

if (pathFieldName !== fieldName) {
return originalError;
}
const pathNumberType = typeof pathNumber;
if (pathNumberType !== 'number') {
return originalError;
}

return relocatedError(originalError, originalError.path.slice(0, 0).concat(originalError.path.slice(2)));
},
const { fieldName, context, info } = delegationContext;

const transformer = new Transformer({
...delegationContext,
args: argsFromKeys(keys),
...(lazyOptionsFn == null ? options : lazyOptionsFn(options)),
});

if (results instanceof Error) {
return keys.map(() => results);
const processedRequest = transformer.transformRequest(request);

if (shouldValidateRequest) {
validateRequest(delegationContext, processedRequest.document);
}

const values = valuesFromResults == null ? results : valuesFromResults(results, keys);
const executor = getExecutor(delegationContext);

const batchResult = (await executor({
...processedRequest,
context,
info,
})) as ExecutionResult;

return Array.isArray(values) ? values : keys.map(() => values);
return splitResult(transformer.transformResult(batchResult), fieldName, keys.length);
};
}

Expand All @@ -60,23 +93,51 @@ const getLoadersMap = memoize2(function getLoadersMap<K, V, C>(
return new Map<string, DataLoader<K, V, C>>();
});

export function getLoader<K = any, V = any, C = K>(options: BatchDelegateOptions<any>): DataLoader<K, V, C> {
const fieldName = options.fieldName ?? options.info.fieldName;
const loaders = getLoadersMap<K, V, C>(options.info.fieldNodes, options.schema);
export function getLoader<K = any, C = K>(options: BatchDelegateOptions<any>): DataLoader<K, ExecutionResult, C> {
const {
info,
operation = getDelegatingOperation(info.parentType, info.schema),
fieldName = info.fieldName,
} = options;

let loader = loaders.get(fieldName);
if (operation !== 'query' && operation !== 'mutation') {
throw new Error(`Batch delegation not possible for operation '${operation}'.`);
}

// Prevents the keys to be passed with the same structure
const dataLoaderOptions: DataLoader.Options<any, any, any> = {
const dataLoaderOptions: DataLoader.Options<K, ExecutionResult, C> = {
cacheKeyFn: defaultCacheKeyFn,
...options.dataLoaderOptions,
};

const loaders = getLoadersMap<K, ExecutionResult, C>(info.fieldNodes, options.schema);
let loader = loaders.get(fieldName);

if (loader === undefined) {
const batchFn = createBatchFn(options);
loader = new DataLoader<K, V, C>(batchFn, dataLoaderOptions);
loader = new DataLoader<K, ExecutionResult, C>(keys => batchFn(keys), dataLoaderOptions);
loaders.set(fieldName, loader);
}

return loader;
}

function splitResult(result: ExecutionResult, fieldName: string, numItems: number): Array<ExecutionResult> {
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,
}));
}
1 change: 0 additions & 1 deletion packages/batch-delegate/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './batchDelegateToSchema';
export * from './createBatchDelegateFn';

export * from './types';
18 changes: 5 additions & 13 deletions packages/batch-delegate/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,15 @@ 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, V = any, C = K>
extends Omit<IDelegateToSchemaOptions<TContext>, 'args'> {
export interface CreateBatchDelegateFnOptions<TContext = Record<string, any>, K = any, V = any, C = K>
extends Partial<Omit<IDelegateToSchemaOptions<TContext>, 'args' | 'info'>> {
dataLoaderOptions?: DataLoader.Options<K, V, C>;
key: K;
argsFromKeys?: (keys: ReadonlyArray<K>) => Record<string, any>;
valuesFromResults?: (results: any, keys: ReadonlyArray<K>) => Array<V>;
lazyOptionsFn?: BatchDelegateOptionsFn;
}

export interface CreateBatchDelegateFnOptions<TContext = Record<string, any>, K = any, V = any, C = K>
extends Partial<Omit<IDelegateToSchemaOptions<TContext>, 'args' | 'info'>> {
export interface BatchDelegateOptions<TContext = Record<string, any>, K = any, V = any, C = K>
extends Omit<IDelegateToSchemaOptions<TContext>, 'args'> {
dataLoaderOptions?: DataLoader.Options<K, V, C>;
key: K | Array<K>;
argsFromKeys?: (keys: ReadonlyArray<K>) => Record<string, any>;
valuesFromResults?: (results: any, keys: ReadonlyArray<K>) => Array<V>;
lazyOptionsFn?: (batchDelegateOptions: BatchDelegateOptions<TContext, K>) => IDelegateToSchemaOptions<TContext>;
}
Loading

0 comments on commit 1daaa4a

Please sign in to comment.