Skip to content

Commit b844c3e

Browse files
committed
refactor batchDelegateToSchema
...to use DataLoader for the actual GraphQL results, rather than the external values This allows us to properly transduce streams. TODO: 1. We should now be able to add the stream directive for batchDelegateToSchema. 2. Refactor error parsing, now we are relying on the onLocatedError option, but this is no longer necessary. Note that additional tests for batched errors are now passing (see #2951) 3. Bring back createBatchDelegateFn, that may be useful 4. The Receiver is now created separately for each list item, which means that the initialPathDepth probably does not match? This will require a separate argument to the Receiver constructor for adjustment 5. Bring back valuesFromResults and fix documentation, we now operate on GraphQL results, so should be fewer pitfalls (see #2829)
1 parent 97c0328 commit b844c3e

File tree

11 files changed

+259
-148
lines changed

11 files changed

+259
-148
lines changed
Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,89 @@
11
import { BatchDelegateOptions } from './types';
22

3+
import { AsyncExecutionResult, ExecutionResult, getNullableType, GraphQLList } from 'graphql';
4+
5+
import {
6+
DelegationContext,
7+
createRequestFromInfo,
8+
externalValueFromResult,
9+
getDelegationContext,
10+
getDelegatingOperation,
11+
Receiver,
12+
} from '@graphql-tools/delegate';
13+
14+
import { isAsyncIterable, relocatedError } from '@graphql-tools/utils';
15+
316
import { getLoader } from './getLoader';
417

5-
export function batchDelegateToSchema(options: BatchDelegateOptions): any {
18+
export async function batchDelegateToSchema(options: BatchDelegateOptions): Promise<any> {
619
const key = options.key;
720
if (key == null) {
821
return null;
922
} else if (Array.isArray(key) && !key.length) {
1023
return [];
1124
}
12-
const loader = getLoader(options);
13-
return Array.isArray(key) ? loader.loadMany(key) : loader.load(key);
25+
26+
const {
27+
info,
28+
operationName,
29+
operation = getDelegatingOperation(info.parentType, info.schema),
30+
fieldName = info.fieldName,
31+
returnType = info.returnType,
32+
selectionSet,
33+
fieldNodes,
34+
} = options;
35+
36+
if (operation !== 'query' && operation !== 'mutation') {
37+
throw new Error(`Batch delegation not possible for operation '${operation}'.`);
38+
}
39+
40+
const request = createRequestFromInfo({
41+
info,
42+
operation,
43+
fieldName,
44+
selectionSet,
45+
fieldNodes,
46+
operationName,
47+
});
48+
49+
const delegationContext = getDelegationContext({
50+
request,
51+
onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)),
52+
...options,
53+
operation,
54+
fieldName,
55+
returnType,
56+
});
57+
58+
const loader = getLoader(options, request, delegationContext);
59+
60+
if (Array.isArray(key)) {
61+
const results = await loader.loadMany(key);
62+
63+
return results.map(result =>
64+
onResult(result, {
65+
...delegationContext,
66+
returnType: (getNullableType(delegationContext.returnType) as GraphQLList<any>).ofType,
67+
})
68+
);
69+
}
70+
71+
const result = await loader.load(key);
72+
return onResult(result, delegationContext);
73+
}
74+
75+
function onResult(
76+
result: Error | ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>,
77+
delegationContext: DelegationContext
78+
): any {
79+
if (result instanceof Error) {
80+
return result;
81+
}
82+
83+
if (isAsyncIterable(result)) {
84+
const receiver = new Receiver(result, delegationContext);
85+
return receiver.getInitialValue();
86+
}
87+
88+
return externalValueFromResult(result, delegationContext);
1489
}

packages/batch-delegate/src/createBatchDelegateFn.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 120 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema, FieldNode } from 'graphql';
1+
import { GraphQLSchema, FieldNode } from 'graphql';
22

33
import DataLoader from 'dataloader';
44

5+
import { SubschemaConfig, Transformer, getExecutor, validateRequest, DelegationContext } from '@graphql-tools/delegate';
56
import {
6-
SubschemaConfig,
7-
Transformer,
8-
createRequestFromInfo,
9-
getDelegationContext,
10-
getDelegatingOperation,
11-
getExecutor,
12-
validateRequest,
13-
Receiver,
14-
externalValueFromResult,
15-
} from '@graphql-tools/delegate';
16-
import { isAsyncIterable, relocatedError } from '@graphql-tools/utils';
7+
AsyncExecutionResult,
8+
ExecutionPatchResult,
9+
ExecutionResult,
10+
isAsyncIterable,
11+
mapAsyncIterator,
12+
Request,
13+
splitAsyncIterator,
14+
} from '@graphql-tools/utils';
1715

1816
import { BatchDelegateOptions } from './types';
1917

@@ -22,48 +20,21 @@ const cache1: WeakMap<
2220
WeakMap<GraphQLSchema | SubschemaConfig, Record<string, DataLoader<any, any>>>
2321
> = new WeakMap();
2422

25-
function createBatchFn<K = any>(options: BatchDelegateOptions) {
23+
function createBatchFn<K = any>(options: BatchDelegateOptions, request: Request, delegationContext: DelegationContext) {
2624
const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray<K>) => ({ ids: keys }));
27-
const { lazyOptionsFn } = options;
2825

29-
return async (keys: ReadonlyArray<K>) => {
30-
const {
31-
context,
32-
info,
33-
operationName,
34-
operation = getDelegatingOperation(info.parentType, info.schema),
35-
fieldName = info.fieldName,
36-
returnType = new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType),
37-
selectionSet,
38-
fieldNodes,
39-
binding,
40-
skipValidation,
41-
} = options;
42-
43-
if (operation !== 'query' && operation !== 'mutation') {
44-
throw new Error(`Batch delegation not possible for operation '${operation}'.`);
45-
}
26+
const { binding, skipValidation } = options;
4627

47-
const request = createRequestFromInfo({
48-
info,
49-
operation,
50-
fieldName,
51-
selectionSet,
52-
fieldNodes,
53-
operationName,
54-
});
28+
const { fieldName, context, info } = delegationContext;
5529

56-
const delegationContext = getDelegationContext({
57-
request,
58-
args: argsFromKeys(keys),
59-
onLocatedError: originalError => relocatedError(originalError, originalError.path.slice(1)),
60-
...(lazyOptionsFn == null ? options : lazyOptionsFn(options)),
61-
operation,
62-
fieldName,
63-
returnType,
64-
});
65-
66-
const transformer = new Transformer(delegationContext, binding);
30+
return async (keys: ReadonlyArray<K>) => {
31+
const transformer = new Transformer(
32+
{
33+
...delegationContext,
34+
args: argsFromKeys(keys),
35+
},
36+
binding
37+
);
6738

6839
const processedRequest = transformer.transformRequest(request);
6940

@@ -79,40 +50,38 @@ function createBatchFn<K = any>(options: BatchDelegateOptions) {
7950
info,
8051
});
8152

53+
const numKeys = keys.length;
8254
if (isAsyncIterable(batchResult)) {
83-
// TODO: split the asyncIterable and make a new receiver from each of them, return the Receiver instead of the
84-
// initial value, so that the correct info can be used to instantiate the Receiver
85-
const receiver = new Receiver(batchResult, delegationContext, executionResult =>
86-
transformer.transformResult(executionResult)
87-
);
88-
89-
const batchValue = await receiver.getInitialValue();
90-
91-
return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue);
55+
const mappedBatchResult = mapAsyncIterator(batchResult, result => transformer.transformResult(result));
56+
return splitAsyncIterator(mappedBatchResult, numKeys, result => splitAsyncResult(result, fieldName));
9257
}
9358

94-
// TODO: split the batchedResult and return the result instead of the value, so the correct info
95-
// can be used to instantiate the value
96-
const batchValue = externalValueFromResult(transformer.transformResult(batchResult), delegationContext);
97-
98-
return Array.isArray(batchValue) ? batchValue : keys.map(() => batchValue);
59+
return splitResult(transformer.transformResult(batchResult), fieldName, numKeys);
9960
};
10061
}
10162

102-
export function getLoader<K = any, V = any, C = K>(options: BatchDelegateOptions): DataLoader<K, V, C> {
63+
export function getLoader<K = any, C = K>(
64+
options: BatchDelegateOptions,
65+
request: Request,
66+
delegationContext: DelegationContext
67+
): DataLoader<K, ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>, C> {
10368
const fieldName = options.fieldName ?? options.info.fieldName;
10469

105-
let cache2: WeakMap<GraphQLSchema | SubschemaConfig, Record<string, DataLoader<K, V, C>>> = cache1.get(
106-
options.info.fieldNodes
107-
);
70+
let cache2: WeakMap<
71+
GraphQLSchema | SubschemaConfig,
72+
Record<string, DataLoader<K, ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>, C>>
73+
> = cache1.get(options.info.fieldNodes);
10874

10975
if (cache2 === undefined) {
11076
cache2 = new WeakMap();
11177
cache1.set(options.info.fieldNodes, cache2);
11278
const loaders = Object.create(null);
11379
cache2.set(options.schema, loaders);
114-
const batchFn = createBatchFn(options);
115-
const loader = new DataLoader<K, V, C>(keys => batchFn(keys), options.dataLoaderOptions);
80+
const batchFn = createBatchFn(options, request, delegationContext);
81+
const loader = new DataLoader<K, ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>, C>(
82+
keys => batchFn(keys),
83+
options.dataLoaderOptions
84+
);
11685
loaders[fieldName] = loader;
11786
return loader;
11887
}
@@ -122,19 +91,96 @@ export function getLoader<K = any, V = any, C = K>(options: BatchDelegateOptions
12291
if (loaders === undefined) {
12392
loaders = Object.create(null);
12493
cache2.set(options.schema, loaders);
125-
const batchFn = createBatchFn(options);
126-
const loader = new DataLoader<K, V, C>(keys => batchFn(keys), options.dataLoaderOptions);
94+
const batchFn = createBatchFn(options, request, delegationContext);
95+
const loader = new DataLoader<K, ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>, C>(
96+
keys => batchFn(keys),
97+
options.dataLoaderOptions
98+
);
12799
loaders[fieldName] = loader;
128100
return loader;
129101
}
130102

131103
let loader = loaders[fieldName];
132104

133105
if (loader === undefined) {
134-
const batchFn = createBatchFn(options);
135-
loader = new DataLoader<K, V, C>(keys => batchFn(keys), options.dataLoaderOptions);
106+
const batchFn = createBatchFn(options, request, delegationContext);
107+
loader = new DataLoader<K, ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>, C>(
108+
keys => batchFn(keys),
109+
options.dataLoaderOptions
110+
);
136111
loaders[fieldName] = loader;
137112
}
138113

139114
return loader;
140115
}
116+
117+
function splitResult(result: ExecutionResult, fieldName: string, numItems: number): Array<ExecutionResult> {
118+
const { data, errors } = result;
119+
const fieldData = data?.[fieldName];
120+
121+
if (fieldData === undefined) {
122+
if (errors === undefined) {
123+
return Array(numItems).fill({});
124+
}
125+
126+
return Array(numItems).fill({ errors });
127+
}
128+
129+
return fieldData.map((value: any) => ({
130+
data: {
131+
[fieldName]: value,
132+
},
133+
errors,
134+
}));
135+
}
136+
137+
function splitAsyncResult(result: AsyncExecutionResult, fieldName: string): [[number, AsyncExecutionResult]] {
138+
const { data, errors, path } = result as ExecutionPatchResult;
139+
140+
if (path === undefined || path.length === 0) {
141+
const fieldData = data?.[fieldName];
142+
if (fieldData !== undefined) {
143+
return fieldData.map((value: any, index: number) => [
144+
index,
145+
{
146+
data: {
147+
[fieldName]: value,
148+
},
149+
errors,
150+
},
151+
]);
152+
}
153+
} else if (path[0] === fieldName) {
154+
const index = path[1] as number;
155+
156+
if (path.length === 2) {
157+
return [
158+
[
159+
index,
160+
{
161+
...result,
162+
data: {
163+
[fieldName]: data,
164+
},
165+
errors,
166+
},
167+
],
168+
];
169+
}
170+
171+
const newPath = [fieldName, ...path.slice(2)];
172+
return [
173+
[
174+
index,
175+
{
176+
...result,
177+
data,
178+
errors,
179+
path: newPath,
180+
},
181+
],
182+
];
183+
}
184+
185+
return [[undefined, result]];
186+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from './batchDelegateToSchema';
2-
export * from './createBatchDelegateFn';
32

43
export * from './types';

packages/batch-delegate/src/types.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,4 @@ export interface BatchDelegateOptions<TContext = Record<string, any>, K = any, V
1515
dataLoaderOptions?: DataLoader.Options<K, V, C>;
1616
key: K;
1717
argsFromKeys?: (keys: ReadonlyArray<K>) => Record<string, any>;
18-
lazyOptionsFn?: BatchDelegateOptionsFn;
19-
}
20-
21-
export interface CreateBatchDelegateFnOptions<TContext = Record<string, any>, K = any, V = any, C = K>
22-
extends Partial<Omit<IDelegateToSchemaOptions<TContext>, 'args' | 'info'>> {
23-
dataLoaderOptions?: DataLoader.Options<K, V, C>;
24-
argsFromKeys?: (keys: ReadonlyArray<K>) => Record<string, any>;
25-
lazyOptionsFn?: (batchDelegateOptions: BatchDelegateOptions<TContext, K>) => IDelegateToSchemaOptions<TContext>;
2618
}

0 commit comments

Comments
 (0)