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 22, 2021
1 parent 14a8c22 commit 5560fea
Show file tree
Hide file tree
Showing 36 changed files with 1,496 additions and 906 deletions.
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.

139 changes: 101 additions & 38 deletions packages/batch-delegate/src/getLoader.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
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,
DelegationContext,
validateRequest,
getExecutor,
getDelegatingOperation,
createRequestFromInfo,
getDelegationContext,
} from '@graphql-tools/delegate';
import { ExecutionRequest, 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>,
request: ExecutionRequest,
delegationContext: DelegationContext<any>
) => Promise<Array<ExecutionResult<Record<string, any>>>> {
const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray<K>) => ({ ids: keys }));
const fieldName = options.fieldName ?? options.info.fieldName;
const { valuesFromResults, lazyOptionsFn } = 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 { validateRequest: shouldValidateRequest } = options;

return async function batchFn(
keys: ReadonlyArray<K>,
request: ExecutionRequest,
delegationContext: DelegationContext<any>
) {
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);

return Array.isArray(values) ? values : keys.map(() => values);
const batchResult = (await executor({
...processedRequest,
context,
info,
})) as ExecutionResult;

return splitResult(transformer.transformResult(batchResult), fieldName, keys.length);
};
}

Expand All @@ -60,23 +71,75 @@ 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,
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}'.`);
}

let loader = loaders.get(fieldName);
const request = createRequestFromInfo({
info,
operation,
fieldName,
selectionSet,
fieldNodes,
operationName,
});

const delegationContext = getDelegationContext({
request,
...options,
operation,
fieldName,
returnType,
});

// 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, request, delegationContext),
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 5560fea

Please sign in to comment.