Skip to content

Commit

Permalink
add tests from #2951
Browse files Browse the repository at this point in the history
and fix them by reworking batch delegation
  • Loading branch information
yaacovCR committed Jul 29, 2021
1 parent 39cc984 commit e739162
Show file tree
Hide file tree
Showing 23 changed files with 482 additions and 324 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 e739162

Please sign in to comment.