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

create memoized, static buidDelegationPlan

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: this function could be extracted to work on the stiched schema itself rather than the extracted metadata, and might be useful as part of a graphiql-type interface
  • Loading branch information
yaacovCR committed Aug 6, 2021
1 parent 9f551bd commit c19b07a
Show file tree
Hide file tree
Showing 39 changed files with 1,549 additions and 1,106 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.

161 changes: 108 additions & 53 deletions packages/batch-delegate/src/getLoader.ts
Original file line number Diff line number Diff line change
@@ -1,100 +1,155 @@
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 { relocatedError } from '@graphql-tools/utils';
import {
SubschemaConfig,
Transformer,
DelegationContext,
validateRequest,
getExecutor,
getDelegatingOperation,
createRequestFromInfo,
getDelegationContext,
} from '@graphql-tools/delegate';
import { ExecutionRequest, ExecutionResult } from '@graphql-tools/utils';

import { BatchDelegateOptions } from './types';

const cache1: WeakMap<
ReadonlyArray<FieldNode>,
WeakMap<GraphQLSchema | SubschemaConfig<any, any, any, any>, Record<string, DataLoader<any, any>>>
WeakMap<GraphQLSchema | SubschemaConfig, Record<string, DataLoader<any, any>>>
> = new WeakMap();

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 (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 (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);

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);
};
}

const cacheKeyFn = (key: any) => (typeof key === 'object' ? JSON.stringify(key) : key);

export function getLoader<K = any, V = any, C = K>(options: BatchDelegateOptions<any>): DataLoader<K, V, C> {
const fieldName = options.fieldName ?? options.info.fieldName;

let cache2: WeakMap<GraphQLSchema | SubschemaConfig, Record<string, DataLoader<K, V, C>>> | undefined = cache1.get(
options.info.fieldNodes
);
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}'.`);
}

// Prevents the keys to be passed with the same structure
const dataLoaderOptions: DataLoader.Options<any, any, any> = {
cacheKeyFn,
...options.dataLoaderOptions,
};
const request = createRequestFromInfo({
info,
operation,
fieldName,
selectionSet,
fieldNodes,
operationName,
});

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

let cache2 = cache1.get(options.info.fieldNodes);

if (cache2 === undefined) {
cache2 = new WeakMap();
cache1.set(options.info.fieldNodes, cache2);
const loaders = Object.create(null);
cache2.set(options.schema, loaders);
const batchFn = createBatchFn(options);
const loader = new DataLoader<K, V, C>(keys => batchFn(keys), dataLoaderOptions);
const loader = new DataLoader<K, ExecutionResult, C>(
keys => batchFn(keys, request, delegationContext),
options.dataLoaderOptions
);
loaders[fieldName] = loader;
return loader;
}

let loaders = cache2.get(options.schema);
const loaders = cache2.get(options.schema);

if (loaders === undefined) {
loaders = Object.create(null) as Record<string, DataLoader<K, V, C>>;
cache2.set(options.schema, loaders);
const newLoaders = Object.create(null);
cache2.set(options.schema, newLoaders);
const batchFn = createBatchFn(options);
const loader = new DataLoader<K, V, C>(keys => batchFn(keys), dataLoaderOptions);
loaders[fieldName] = loader;
const loader = new DataLoader<K, ExecutionResult, C>(
keys => batchFn(keys, request, delegationContext),
options.dataLoaderOptions
);
newLoaders[fieldName] = loader;
return loader;
}

let loader = loaders[fieldName];

if (loader === undefined) {
const batchFn = createBatchFn(options);
loader = new DataLoader<K, V, C>(keys => batchFn(keys), dataLoaderOptions);
loader = new DataLoader<K, ExecutionResult, C>(
keys => batchFn(keys, request, delegationContext),
options.dataLoaderOptions
);
loaders[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';
28 changes: 6 additions & 22 deletions packages/batch-delegate/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,20 @@
import { FieldNode, GraphQLSchema } from 'graphql';

import DataLoader from 'dataloader';

import { IDelegateToSchemaOptions, SubschemaConfig } from '@graphql-tools/delegate';

// TODO: remove in next major release
export type DataLoaderCache<K = any, V = any, C = K> = WeakMap<
ReadonlyArray<FieldNode>,
WeakMap<GraphQLSchema | SubschemaConfig, DataLoader<K, V, C>>
>;
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, 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 c19b07a

Please sign in to comment.