Skip to content

Commit 421166b

Browse files
committed
defer proxying proof of concept
1 parent 0dedab2 commit 421166b

File tree

14 files changed

+231
-26
lines changed

14 files changed

+231
-26
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,6 @@
8383
"./website"
8484
],
8585
"resolutions": {
86-
"graphql": "15.3.0"
86+
"graphql": "npm:graphql-experimental"
8787
}
8888
}

packages/delegate/src/defaultMergedResolver.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defaultFieldResolver, GraphQLResolveInfo } from 'graphql';
22

3-
import { getResponseKeyFromInfo } from '@graphql-tools/utils';
3+
import { getResponseKeyFromInfo, ExecutionResult } from '@graphql-tools/utils';
44

55
import { resolveExternalValue } from './resolveExternalValue';
66
import { getSubschema } from './Subschema';
@@ -18,7 +18,7 @@ export function defaultMergedResolver(
1818
args: Record<string, any>,
1919
context: Record<string, any>,
2020
info: GraphQLResolveInfo
21-
) {
21+
): any {
2222
if (!parent) {
2323
return null;
2424
}
@@ -33,7 +33,27 @@ export function defaultMergedResolver(
3333

3434
const data = parent[responseKey];
3535
const unpathedErrors = getUnpathedErrors(parent);
36+
37+
// To Do:
38+
// add support for transforms
39+
// call out to Receiver abstraction that will publish all patches with channel based on path
40+
// edit code below to subscribe to appropriate channel based on path
41+
// so as to handle multiple defer patches and discriminate between them without need for labels
42+
43+
if (data === undefined && 'ASYNC_ITERABLE' in parent) {
44+
const asyncIterable = parent['ASYNC_ITERABLE'];
45+
return asyncIterableToResult(asyncIterable).then(patch => {
46+
return defaultMergedResolver(patch.data, args, context, info);
47+
});
48+
}
49+
3650
const subschema = getSubschema(parent, responseKey);
3751

3852
return resolveExternalValue(data, unpathedErrors, subschema, context, info);
3953
}
54+
55+
async function asyncIterableToResult(asyncIterable: AsyncIterable<ExecutionResult>): Promise<any> {
56+
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
57+
const payload = await asyncIterator.next();
58+
return payload.value;
59+
}

packages/delegate/src/delegateToSchema.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
subscribe,
3-
execute,
43
validate,
54
GraphQLSchema,
65
isSchema,
@@ -13,9 +12,11 @@ import {
1312
GraphQLObjectType,
1413
} from 'graphql';
1514

15+
import { execute } from 'graphql/experimental';
16+
1617
import isPromise from 'is-promise';
1718

18-
import { mapAsyncIterator, ExecutionResult } from '@graphql-tools/utils';
19+
import { mapAsyncIterator, ExecutionResult, isAsyncIterable } from '@graphql-tools/utils';
1920

2021
import {
2122
IDelegateToSchemaOptions,
@@ -25,6 +26,7 @@ import {
2526
StitchingInfo,
2627
Endpoint,
2728
Transform,
29+
Executor,
2830
} from './types';
2931

3032
import { isSubschemaConfig } from './Subschema';
@@ -189,8 +191,16 @@ export function delegateRequest({
189191
info,
190192
});
191193

192-
if (isPromise(executionResult)) {
193-
return executionResult.then(originalResult => transformer.transformResult(originalResult));
194+
if (isAsyncIterable(executionResult)) {
195+
return asyncIterableToResult(executionResult).then(originalResult => {
196+
const transformedResult = transformer.transformResult(originalResult);
197+
transformedResult['ASYNC_ITERABLE'] = executionResult;
198+
return transformedResult;
199+
});
200+
} else if (isPromise(executionResult)) {
201+
return (executionResult as Promise<ExecutionResult>).then(originalResult =>
202+
transformer.transformResult(originalResult)
203+
);
194204
}
195205
return transformer.transformResult(executionResult);
196206
}
@@ -203,7 +213,7 @@ export function delegateRequest({
203213
context,
204214
info,
205215
}).then((subscriptionResult: AsyncIterableIterator<ExecutionResult> | ExecutionResult) => {
206-
if (Symbol.asyncIterator in subscriptionResult) {
216+
if (isAsyncIterable(subscriptionResult)) {
207217
// "subscribe" to the subscription result and map the result through the transforms
208218
return mapAsyncIterator<ExecutionResult, any>(
209219
subscriptionResult as AsyncIterableIterator<ExecutionResult>,
@@ -229,15 +239,15 @@ function validateRequest(targetSchema: GraphQLSchema, document: DocumentNode) {
229239
}
230240
}
231241

232-
function createDefaultExecutor(schema: GraphQLSchema, rootValue: Record<string, any>) {
233-
return ({ document, context, variables, info }: ExecutionParams) =>
242+
function createDefaultExecutor(schema: GraphQLSchema, rootValue: Record<string, any>): Executor {
243+
return (({ document, context, variables, info }: ExecutionParams) =>
234244
execute({
235245
schema,
236246
document,
237247
contextValue: context,
238248
variableValues: variables,
239249
rootValue: rootValue ?? info?.rootValue,
240-
});
250+
})) as Executor;
241251
}
242252

243253
function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record<string, any>) {
@@ -250,3 +260,9 @@ function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record<string
250260
rootValue: rootValue ?? info?.rootValue,
251261
}) as any;
252262
}
263+
264+
async function asyncIterableToResult(asyncIterable: AsyncIterable<ExecutionResult>): Promise<any> {
265+
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
266+
const payload = await asyncIterator.next();
267+
return payload.value;
268+
}

packages/delegate/src/getBatchingExecutor.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import isPromise from 'is-promise';
44

55
import DataLoader from 'dataloader';
66

7-
import { ExecutionResult } from '@graphql-tools/utils';
7+
import { ExecutionResult, isAsyncIterable } from '@graphql-tools/utils';
88

9-
import { ExecutionParams, Endpoint } from './types';
9+
import { ExecutionParams, Endpoint, Executor } from './types';
1010
import { memoize2of3 } from './memoize';
1111
import { mergeExecutionParams } from './mergeExecutionParams';
1212
import { splitResult } from './splitResult';
1313

1414
export const getBatchingExecutor = memoize2of3(function (
1515
_context: Record<string, any>,
1616
endpoint: Endpoint,
17-
executor: ({ document, context, variables, info }: ExecutionParams) => ExecutionResult | Promise<ExecutionResult>
17+
executor: Executor
1818
) {
1919
const loader = new DataLoader(
2020
createLoadFn(
@@ -27,7 +27,7 @@ export const getBatchingExecutor = memoize2of3(function (
2727
});
2828

2929
function createLoadFn(
30-
executor: ({ document, context, variables, info }: ExecutionParams) => ExecutionResult | Promise<ExecutionResult>,
30+
executor: Executor,
3131
extensionsReducer: (mergedExtensions: Record<string, any>, executionParams: ExecutionParams) => Record<string, any>
3232
) {
3333
return async (execs: Array<ExecutionParams>): Promise<Array<ExecutionResult>> => {
@@ -53,10 +53,16 @@ function createLoadFn(
5353
const mergedExecutionParams = mergeExecutionParams(execBatch, extensionsReducer);
5454
const executionResult = executor(mergedExecutionParams);
5555

56-
if (isPromise(executionResult)) {
56+
if (isAsyncIterable(executionResult)) {
57+
throw new Error('batching not yet possible with queries that return an async iterable (defer/stream)');
58+
// requires splitting up the async iterable into multiple async iterables by path versus possibly just promises
59+
// so requires analyzing which of the results would get an async iterable (ie included defer/stream within the subdocument)
60+
// or returning an async iterable even though defer/stream was not actually present, which is probably simpler
61+
// but most probably against the spec.
62+
} else if (isPromise(executionResult)) {
5763
containsPromises = true;
5864
}
59-
executionResults.push(executionResult);
65+
executionResults.push(executionResult as ExecutionResult);
6066
});
6167

6268
if (containsPromises) {

packages/delegate/src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
GraphQLError,
1515
} from 'graphql';
1616

17+
import { AsyncExecutionResult } from 'graphql/experimental';
18+
1719
import DataLoader from 'dataloader';
1820

1921
import { Operation, Request, TypeMap, ExecutionResult } from '@graphql-tools/utils';
@@ -131,7 +133,9 @@ export type AsyncExecutor = <
131133
export type SyncExecutor = <TReturn = Record<string, any>, TArgs = Record<string, any>, TContext = Record<string, any>>(
132134
params: ExecutionParams<TArgs, TContext>
133135
) => ExecutionResult<TReturn>;
134-
export type Executor = AsyncExecutor | SyncExecutor;
136+
export type Executor = <TReturn = Record<string, any>, TArgs = Record<string, any>, TContext = Record<string, any>>(
137+
params: ExecutionParams<TArgs, TContext>
138+
) => Promise<ExecutionResult<TReturn>> | ExecutionResult<TReturn> | AsyncIterable<AsyncExecutionResult>;
135139
export type Subscriber = <TReturn = Record<string, any>, TArgs = Record<string, any>, TContext = Record<string, any>>(
136140
params: ExecutionParams<TArgs, TContext>
137141
) => Promise<AsyncIterator<ExecutionResult<TReturn>> | ExecutionResult<TReturn>>;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { graphql } from 'graphql/experimental';
2+
3+
import { makeExecutableSchema } from '@graphql-tools/schema';
4+
import { stitchSchemas } from '@graphql-tools/stitch';
5+
import { isAsyncIterable } from '@graphql-tools/utils';
6+
7+
describe('defer support', () => {
8+
test('should work for root fields', async () => {
9+
const schema = makeExecutableSchema({
10+
typeDefs: `
11+
type Query {
12+
test: String
13+
}
14+
`,
15+
resolvers: {
16+
Query: {
17+
test: () => 'test',
18+
}
19+
},
20+
});
21+
22+
const stitchedSchema = stitchSchemas({
23+
subschemas: [schema]
24+
});
25+
26+
const result = await graphql(
27+
stitchedSchema,
28+
`
29+
query {
30+
... on Query @defer {
31+
test
32+
}
33+
}
34+
`,
35+
);
36+
37+
const results = [];
38+
if (isAsyncIterable(result)) {
39+
for await (let patch of result) {
40+
results.push(patch);
41+
}
42+
}
43+
44+
expect(results[0]).toEqual({
45+
data: {},
46+
hasNext: true,
47+
});
48+
expect(results[1]).toEqual({
49+
data: {
50+
test: 'test'
51+
},
52+
hasNext: false,
53+
path: [],
54+
});
55+
});
56+
57+
test('should work for nested fields', async () => {
58+
const schema = makeExecutableSchema({
59+
typeDefs: `
60+
type Object {
61+
test: String
62+
}
63+
type Query {
64+
object: Object
65+
}
66+
`,
67+
resolvers: {
68+
Object: {
69+
test: () => 'test',
70+
},
71+
Query: {
72+
object: () => ({}),
73+
}
74+
},
75+
});
76+
77+
const stitchedSchema = stitchSchemas({
78+
subschemas: [schema]
79+
});
80+
81+
const result = await graphql(
82+
stitchedSchema,
83+
`
84+
query {
85+
object {
86+
... on Object @defer {
87+
test
88+
}
89+
}
90+
}
91+
`,
92+
);
93+
94+
const results = [];
95+
if (isAsyncIterable(result)) {
96+
for await (let patch of result) {
97+
results.push(patch);
98+
}
99+
}
100+
101+
expect(results[0]).toEqual({
102+
data: { object: {} },
103+
hasNext: true,
104+
});
105+
expect(results[1]).toEqual({
106+
data: {
107+
test: 'test'
108+
},
109+
hasNext: false,
110+
path: ['object'],
111+
});
112+
});
113+
});
114+

packages/load/tests/loaders/schema/integration.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,31 @@ describe('loadSchema', () => {
5353
const schemaStr = printSchema(schema);
5454

5555
expect(schemaStr).toBeSimilarGqlDoc(/* GraphQL */`
56+
"""
57+
Directs the executor to defer this fragment when the \`if\` argument is true or undefined.
58+
"""
59+
directive @defer(
60+
"""Deferred when true or undefined."""
61+
if: Boolean
62+
63+
"""Unique name"""
64+
label: String
65+
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
66+
67+
"""
68+
Directs the executor to stream plural fields when the \`if\` argument is true or undefined.
69+
"""
70+
directive @stream(
71+
"""Stream when true or undefined."""
72+
if: Boolean
73+
74+
"""Unique name"""
75+
label: String
76+
77+
"""Number of items to return immediately"""
78+
initialCount: Int!
79+
) on FIELD
80+
5681
type Query {
5782
a: A
5883
b: B

packages/schema/src/buildSchemaFromTypeDefinitions.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extendSchema, buildASTSchema, GraphQLSchema, DocumentNode, ASTNode } from 'graphql';
1+
import { extendSchema, buildASTSchema, GraphQLSchema, DocumentNode, ASTNode, BuildSchemaOptions } from 'graphql';
22

33
import { ITypeDefinitions, GraphQLParseOptions, parseGraphQLSDL } from '@graphql-tools/utils';
44

@@ -12,12 +12,16 @@ export function buildSchemaFromTypeDefinitions(
1212
const document = buildDocumentFromTypeDefinitions(typeDefinitions, parseOptions);
1313
const typesAst = filterExtensionDefinitions(document);
1414

15-
const backcompatOptions = { commentDescriptions: true };
16-
let schema: GraphQLSchema = buildASTSchema(typesAst, backcompatOptions);
15+
const options: BuildSchemaOptions = {
16+
commentDescriptions: true,
17+
experimentalDefer: true,
18+
experimentalStream: true,
19+
};
20+
let schema: GraphQLSchema = buildASTSchema(typesAst, options);
1721

1822
const extensionsAst = extractExtensionDefinitions(document);
1923
if (extensionsAst.definitions.length > 0) {
20-
schema = extendSchema(schema, extensionsAst, backcompatOptions);
24+
schema = extendSchema(schema, extensionsAst, options);
2125
}
2226

2327
return schema;

packages/schema/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,12 @@ export interface IExecutableSchemaDefinition<TContext = any> {
6767
* Additional options for removing unused types from the schema
6868
*/
6969
pruningOptions?: PruneSchemaOptions;
70+
/**
71+
* Set to `true` to enable support within queries for the experimental `defer` directive
72+
*/
73+
experimentalDefer?: boolean;
74+
/**
75+
* Set to `true` to enable support within queries for the experimental `stream` directive
76+
*/
77+
experimentalStream?: boolean;
7078
}

0 commit comments

Comments
 (0)