Skip to content

Commit 406c4d2

Browse files
committed
Improvements on runtime and JIT logic
1 parent 181f0e4 commit 406c4d2

File tree

13 files changed

+186
-84
lines changed

13 files changed

+186
-84
lines changed

.changeset/grumpy-onions-fold.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-mesh/transform-federation': patch
3+
---
4+
5+
Remove other directives in scalars just like it is done for objects and other types

.changeset/hot-bugs-hammer.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-mesh/runtime': patch
3+
---
4+
5+
Simplify the logic and use GraphQL Tools executor

.changeset/many-turkeys-share.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-mesh/grpc': patch
3+
---
4+
5+
Add response streams as subscriptions

.changeset/seven-bags-exist.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-mesh/runtime': patch
3+
---
4+
5+
Do not cache entire request but only DocumentNode
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
subscription SearchMoviesByCast {
2+
exampleSearchMoviesByCast(input: { castName: "Tom Cruise" }) {
3+
name
4+
year
5+
rating
6+
cast
7+
}
8+
}

examples/grpc-example/tests/__snapshots__/grpc.test.ts.snap

+46-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-0 1`] = `
3+
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-stream-0 1`] = `
44
{
55
"data": {
66
"exampleSearchMoviesByCast": [],
@@ -9,7 +9,7 @@ exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-
99
}
1010
`;
1111

12-
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-1 1`] = `
12+
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-stream-1 1`] = `
1313
{
1414
"hasNext": true,
1515
"incremental": [
@@ -35,7 +35,7 @@ exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-
3535
}
3636
`;
3737

38-
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-2 1`] = `
38+
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-stream-2 1`] = `
3939
{
4040
"hasNext": true,
4141
"incremental": [
@@ -61,15 +61,50 @@ exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-
6161
}
6262
`;
6363

64-
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-3 1`] = `
64+
exports[`gRPC Example should fetch movies by cast as a stream correctly: movies-by-cast-grpc-example-result-stream-3 1`] = `
6565
{
6666
"hasNext": false,
6767
}
6868
`;
6969

70+
exports[`gRPC Example should fetch movies by cast as a subscription correctly: movies-by-cast-grpc-example-result-subscription-0 1`] = `
71+
{
72+
"data": {
73+
"exampleSearchMoviesByCast": {
74+
"cast": [
75+
"Tom Cruise",
76+
"Simon Pegg",
77+
"Jeremy Renner",
78+
],
79+
"name": "Mission: Impossible Rogue Nation",
80+
"rating": 0.9700000286102295,
81+
"year": 2015,
82+
},
83+
},
84+
}
85+
`;
86+
87+
exports[`gRPC Example should fetch movies by cast as a subscription correctly: movies-by-cast-grpc-example-result-subscription-1 1`] = `
88+
{
89+
"data": {
90+
"exampleSearchMoviesByCast": {
91+
"cast": [
92+
"Tom Cruise",
93+
"Simon Pegg",
94+
"Henry Cavill",
95+
],
96+
"name": "Mission: Impossible - Fallout",
97+
"rating": 0.9300000071525574,
98+
"year": 2018,
99+
},
100+
},
101+
}
102+
`;
103+
70104
exports[`gRPC Example should generate correct schema: grpc-schema 1`] = `
71105
"schema {
72106
query: Query
107+
subscription: Subscription
73108
}
74109
75110
directive @grpcMethod(rootJsonName: String, objPath: String, methodName: String, responseStream: Boolean) on FIELD_DEFINITION
@@ -166,6 +201,13 @@ enum ConnectivityState {
166201
SHUTDOWN
167202
}
168203
204+
type Subscription {
205+
"search movies by the name of the cast"
206+
exampleSearchMoviesByCast(input: SearchByCastRequest_Input): Movie @grpcMethod(rootJsonName: "Root0", objPath: "Example", methodName: "SearchMoviesByCast", responseStream: true)
207+
"search movies by the name of the cast"
208+
anotherExampleSearchMoviesByCast(input: SearchByCastRequest_Input): Movie @grpcMethod(rootJsonName: "Root0", objPath: "AnotherExample", methodName: "SearchMoviesByCast", responseStream: true)
209+
}
210+
169211
scalar ObjMap"
170212
`;
171213

examples/grpc-example/tests/grpc.test.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ describe('gRPC Example', () => {
3636
const result = await mesh.execute(MoviesByCastStream, undefined);
3737
let i = 0;
3838
for await (const item of result as AsyncIterable<any>) {
39-
expect(item).toMatchSnapshot(`movies-by-cast-grpc-example-result-${i++}`);
39+
expect(item).toMatchSnapshot(`movies-by-cast-grpc-example-result-stream-${i++}`);
40+
}
41+
});
42+
it('should fetch movies by cast as a subscription correctly', async () => {
43+
const MoviesByCastSubscription = await readFile(
44+
join(__dirname, '../example-queries/MoviesByCast.subscription.graphql'),
45+
'utf8',
46+
);
47+
const result = await mesh.execute(MoviesByCastSubscription, undefined);
48+
let i = 0;
49+
for await (const item of result as AsyncIterable<any>) {
50+
expect(item).toMatchSnapshot(`movies-by-cast-grpc-example-result-subscription-${i++}`);
4051
}
4152
});
4253
afterAll(async () => {

packages/handlers/grpc/src/index.ts

+54-19
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,22 @@ export default class GrpcHandler implements MeshHandler {
419419
objPath,
420420
creds,
421421
});
422-
field.resolve = this.getFieldResolver({
423-
client,
424-
methodName,
425-
isResponseStream: responseStream,
426-
});
422+
if (rootType.name === 'Subscription') {
423+
field.subscribe = this.getFieldResolver({
424+
client,
425+
methodName,
426+
isResponseStream: responseStream,
427+
});
428+
field.resolve = function identityFn(root) {
429+
return root;
430+
};
431+
} else {
432+
field.resolve = this.getFieldResolver({
433+
client,
434+
methodName,
435+
isResponseStream: responseStream,
436+
});
437+
}
427438
break;
428439
}
429440
case 'grpcConnectivityState': {
@@ -598,26 +609,29 @@ export default class GrpcHandler implements MeshHandler {
598609
for (const methodName in nested.methods) {
599610
const method = nested.methods[methodName];
600611
const rootFieldName = [...pathWithName, methodName].join('_');
612+
const fieldConfigTypeFactory = () => {
613+
const baseResponseTypePath = method.responseType?.split('.');
614+
if (baseResponseTypePath) {
615+
const responseTypePath = this.walkToFindTypePath(
616+
rootJson,
617+
pathWithName,
618+
baseResponseTypePath,
619+
);
620+
return getTypeName(this.schemaComposer, responseTypePath, false);
621+
}
622+
return 'Void';
623+
};
601624
const fieldConfig: ObjectTypeComposerFieldConfigAsObjectDefinition<any, any> = {
602625
type: () => {
603-
const baseResponseTypePath = method.responseType?.split('.');
604-
if (baseResponseTypePath) {
605-
const responseTypePath = this.walkToFindTypePath(
606-
rootJson,
607-
pathWithName,
608-
baseResponseTypePath,
609-
);
610-
let typeName = getTypeName(this.schemaComposer, responseTypePath, false);
611-
if (method.responseStream) {
612-
typeName = `[${typeName}]`;
613-
}
614-
return typeName;
626+
const typeName = fieldConfigTypeFactory();
627+
if (method.responseStream) {
628+
return `[${typeName}]`;
615629
}
616-
return 'Void';
630+
return typeName;
617631
},
618632
description: method.comment,
619633
};
620-
fieldConfig.args = {
634+
const fieldConfigArgs = {
621635
input: () => {
622636
if (method.requestStream) {
623637
return 'File';
@@ -635,6 +649,7 @@ export default class GrpcHandler implements MeshHandler {
635649
return undefined;
636650
},
637651
};
652+
fieldConfig.args = fieldConfigArgs;
638653
const methodNameLowerCased = methodName.toLowerCase();
639654
const prefixQueryMethod = this.config.prefixQueryMethod || QUERY_METHOD_PREFIXES;
640655
const rootTypeComposer = prefixQueryMethod.some(prefix =>
@@ -659,6 +674,26 @@ export default class GrpcHandler implements MeshHandler {
659674
],
660675
},
661676
});
677+
if (method.responseStream) {
678+
this.schemaComposer.Subscription.addFields({
679+
[rootFieldName]: {
680+
args: fieldConfigArgs,
681+
description: method.comment,
682+
type: fieldConfigTypeFactory,
683+
directives: [
684+
{
685+
name: 'grpcMethod',
686+
args: {
687+
rootJsonName,
688+
objPath,
689+
methodName,
690+
responseStream: true,
691+
},
692+
},
693+
],
694+
},
695+
});
696+
}
662697
}
663698
const connectivityStateFieldName = pathWithName.join('_') + '_connectivityState';
664699
this.schemaComposer.addDirective(grpcConnectivityStateDirective);

packages/runtime/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@graphql-mesh/string-interpolation": "^0.5.2",
4747
"@graphql-tools/batch-delegate": "^9.0.0",
4848
"@graphql-tools/delegate": "^10.0.0",
49+
"@graphql-tools/executor": "^1.2.0",
4950
"@graphql-tools/wrap": "^10.0.0",
5051
"@whatwg-node/fetch": "^0.9.0",
5152
"graphql-jit": "0.8.2"

packages/runtime/src/get-mesh.ts

+5-34
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {
22
DocumentNode,
3-
execute,
4-
getOperationAST,
53
GraphQLObjectType,
64
GraphQLSchema,
75
OperationTypeNode,
@@ -35,10 +33,10 @@ import {
3533
PubSub,
3634
} from '@graphql-mesh/utils';
3735
import { CreateProxyingResolverFn, Subschema, SubschemaConfig } from '@graphql-tools/delegate';
36+
import { normalizedExecutor } from '@graphql-tools/executor';
3837
import {
3938
ExecutionResult,
4039
getRootTypeMap,
41-
inspect,
4240
isAsyncIterable,
4341
isPromise,
4442
mapAsyncIterator,
@@ -76,14 +74,6 @@ const memoizedGetEnvelopedFactory = memoize1(function getEnvelopedFactory(
7674
});
7775
});
7876

79-
const memoizedGetOperationType = memoize1((document: DocumentNode) => {
80-
const operationAST = getOperationAST(document, undefined);
81-
if (!operationAST) {
82-
throw new Error('Must provide document with a valid operation');
83-
}
84-
return operationAST.operation;
85-
});
86-
8777
export function wrapFetchWithPlugins(plugins: MeshPlugin<any>[]): MeshFetch {
8878
const onFetchHooks: OnFetchHook<any>[] = [];
8979
for (const plugin of plugins as MeshPlugin<any>[]) {
@@ -92,24 +82,6 @@ export function wrapFetchWithPlugins(plugins: MeshPlugin<any>[]): MeshFetch {
9282
}
9383
}
9484
return function wrappedFetchFn(url, options, context, info) {
95-
if (url != null && typeof url !== 'string') {
96-
throw new TypeError(`First parameter(url) of 'fetch' must be a string, got ${inspect(url)}`);
97-
}
98-
if (options != null && typeof options !== 'object') {
99-
throw new TypeError(
100-
`Second parameter(options) of 'fetch' must be an object, got ${inspect(options)}`,
101-
);
102-
}
103-
if (context != null && typeof context !== 'object') {
104-
throw new TypeError(
105-
`Third parameter(context) of 'fetch' must be an object, got ${inspect(context)}`,
106-
);
107-
}
108-
if (info != null && typeof info !== 'object') {
109-
throw new TypeError(
110-
`Fourth parameter(info) of 'fetch' must be an object, got ${inspect(info)}`,
111-
);
112-
}
11385
let fetchFn: MeshFetch;
11486
const doneHooks: OnFetchHookDone[] = [];
11587
function setFetchFn(newFetchFn: MeshFetch) {
@@ -318,7 +290,7 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
318290

319291
const plugins = [
320292
useEngine({
321-
execute,
293+
execute: normalizedExecutor,
322294
validate,
323295
parse: parseWithCache,
324296
specifiedRules,
@@ -368,7 +340,7 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
368340

369341
function createExecutor(globalContext: any = EMPTY_CONTEXT_VALUE): MeshExecutor {
370342
const getEnveloped = memoizedGetEnvelopedFactory(plugins);
371-
const { schema, parse, execute, subscribe, contextFactory } = getEnveloped(globalContext);
343+
const { schema, parse, execute, contextFactory } = getEnveloped(globalContext);
372344
return function meshExecutor<TVariables = any, TContext = any, TRootValue = any, TData = any>(
373345
documentOrSDL: GraphQLOperation<TData, TVariables>,
374346
variableValues: TVariables = EMPTY_VARIABLES_VALUE,
@@ -377,11 +349,10 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
377349
operationName?: string,
378350
) {
379351
const document = typeof documentOrSDL === 'string' ? parse(documentOrSDL) : documentOrSDL;
380-
const executeFn = memoizedGetOperationType(document) === 'subscription' ? subscribe : execute;
381352
const contextValue$ = contextFactory(contextValue);
382353
if (isPromise(contextValue$)) {
383354
return contextValue$.then(contextValue =>
384-
executeFn({
355+
execute({
385356
schema,
386357
document,
387358
contextValue,
@@ -391,7 +362,7 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
391362
}),
392363
);
393364
}
394-
return executeFn({
365+
return execute({
395366
schema,
396367
document,
397368
contextValue: contextValue$,

0 commit comments

Comments
 (0)