Skip to content

Commit

Permalink
Fix multiple gRPC subgraphs in a supergraph (#8203)
Browse files Browse the repository at this point in the history
* Fix multiple gRPC subgraphs in a supergraph

* Snapshots
  • Loading branch information
ardatan authored Jan 2, 2025
1 parent 18fa74e commit c541164
Show file tree
Hide file tree
Showing 18 changed files with 649 additions and 149 deletions.
11 changes: 11 additions & 0 deletions .changeset/modern-doors-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@graphql-mesh/grpc': minor
'@graphql-mesh/transport-grpc': minor
'@omnigraph/grpc': minor
---

Handle multiple gRPC services correctly in a supergraph

Previously multiple directives on Query type conflicting, which needs to be fixed on Gateway runtime later, but for now, it should be already in the transport directive. And this change fixes the issue before the gateway runtime fix.

Generated schema will be different so this can be considered a breaking change but it will be no functional change for the existing users.
14 changes: 3 additions & 11 deletions e2e/grpc-example/__snapshots__/grpc-example.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ schema
@link(
url: "https://the-guild.dev/graphql/mesh/spec/v1.0"
import: ["@grpcMethod", "@grpcConnectivityState", "@enum", "@grpcRootJson", "@transport", "@source", "@extraSchemaDefinitionDirective"]
import: ["@grpcMethod", "@grpcConnectivityState", "@enum", "@transport", "@source", "@extraSchemaDefinitionDirective"]
)
{
query: Query
Expand Down Expand Up @@ -130,8 +130,6 @@ directive @grpcConnectivityState(subgraph: String, rootJsonName: String, objPath
directive @enum(subgraph: String, value: String) repeatable on ENUM_VALUE
directive @grpcRootJson(subgraph: String, name: String, rootJson: ObjMap, loadOptions: ObjMap) repeatable on OBJECT
directive @transport(subgraph: String, kind: String, location: String, options: TransportOptions) repeatable on SCHEMA
directive @source(name: String!, type: String, subgraph: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
Expand All @@ -143,18 +141,12 @@ The \`BigInt\` scalar type represents non-fractional signed whole numeric values
"""
scalar BigInt @join__type(graph: MOVIES)
scalar ObjMap @join__type(graph: MOVIES)
scalar TransportOptions @join__type(graph: MOVIES)
scalar _DirectiveExtensions @join__type(graph: MOVIES)
type Query @grpcRootJson(
subgraph: "movies"
name: "Root0"
rootJson: "{\\"options\\":{\\"syntax\\":\\"proto3\\"},\\"nested\\":{\\"EmptyRequest\\":{\\"fields\\":{},\\"comment\\":null},\\"MovieRequest\\":{\\"fields\\":{\\"movie\\":{\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"SearchByCastRequest\\":{\\"fields\\":{\\"castName\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"MoviesResult\\":{\\"fields\\":{\\"result\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":\\"list of movies\\"}},\\"comment\\":\\"movie result message, contains list of movies\\"},\\"Example\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"MovieRequest\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null},\\"AnotherExample\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"MovieRequest\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null},\\"Genre\\":{\\"values\\":{\\"UNSPECIFIED\\":0,\\"ACTION\\":1,\\"DRAMA\\":2},\\"comment\\":null,\\"comments\\":{\\"UNSPECIFIED\\":null,\\"ACTION\\":null,\\"DRAMA\\":null}},\\"Movie\\":{\\"fields\\":{\\"name\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null},\\"year\\":{\\"type\\":\\"int64\\",\\"id\\":2,\\"comment\\":null},\\"rating\\":{\\"type\\":\\"float\\",\\"id\\":3,\\"comment\\":null},\\"cast\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"string\\",\\"id\\":4,\\"comment\\":\\"list of cast\\"},\\"time\\":{\\"type\\":\\"google.protobuf.Timestamp\\",\\"id\\":5,\\"comment\\":null},\\"genre\\":{\\"type\\":\\"Genre\\",\\"id\\":6,\\"comment\\":null}},\\"comment\\":\\"movie message payload\\"},\\"google\\":{\\"nested\\":{\\"protobuf\\":{\\"nested\\":{\\"Timestamp\\":{\\"fields\\":{\\"seconds\\":{\\"type\\":\\"int64\\",\\"id\\":1},\\"nanos\\":{\\"type\\":\\"int32\\",\\"id\\":2}},\\"comment\\":null}}}}}}}"
) @extraSchemaDefinitionDirective(
directives: {transport: [{subgraph: "movies", kind: "grpc", location: "localhost:<movies_port>", options: {requestTimeout: 200000, metaData: {someKey: "someValue", connection_type: "{context.headers.connection}"}}}]}
type Query @extraSchemaDefinitionDirective(
directives: {transport: [{subgraph: "movies", kind: "grpc", location: "localhost:<movies_port>", options: {requestTimeout: 200000, metaData: {someKey: "someValue", connection_type: "{context.headers.connection}"}, roots: [{name: "Root0", rootJson: "{\\"options\\":{\\"syntax\\":\\"proto3\\"},\\"nested\\":{\\"EmptyRequest\\":{\\"fields\\":{},\\"comment\\":null},\\"MovieRequest\\":{\\"fields\\":{\\"movie\\":{\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"SearchByCastRequest\\":{\\"fields\\":{\\"castName\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"MoviesResult\\":{\\"fields\\":{\\"result\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":\\"list of movies\\"}},\\"comment\\":\\"movie result message, contains list of movies\\"},\\"Example\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"MovieRequest\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null},\\"AnotherExample\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"MovieRequest\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null},\\"Genre\\":{\\"values\\":{\\"UNSPECIFIED\\":0,\\"ACTION\\":1,\\"DRAMA\\":2},\\"comment\\":null,\\"comments\\":{\\"UNSPECIFIED\\":null,\\"ACTION\\":null,\\"DRAMA\\":null}},\\"Movie\\":{\\"fields\\":{\\"name\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null},\\"year\\":{\\"type\\":\\"int64\\",\\"id\\":2,\\"comment\\":null},\\"rating\\":{\\"type\\":\\"float\\",\\"id\\":3,\\"comment\\":null},\\"cast\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"string\\",\\"id\\":4,\\"comment\\":\\"list of cast\\"},\\"time\\":{\\"type\\":\\"google.protobuf.Timestamp\\",\\"id\\":5,\\"comment\\":null},\\"genre\\":{\\"type\\":\\"Genre\\",\\"id\\":6,\\"comment\\":null}},\\"comment\\":\\"movie message payload\\"},\\"google\\":{\\"nested\\":{\\"protobuf\\":{\\"nested\\":{\\"Timestamp\\":{\\"fields\\":{\\"seconds\\":{\\"type\\":\\"int64\\",\\"id\\":1},\\"nanos\\":{\\"type\\":\\"int32\\",\\"id\\":2}},\\"comment\\":null}}}}}}}"}]}}]}
) @join__type(graph: MOVIES) {
"""
get all movies
Expand Down
267 changes: 267 additions & 0 deletions e2e/grpc-multiple/__snapshots__/grpc-multiple.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`gRPC Multiple composes 1`] = `
"
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
@link(
url: "https://the-guild.dev/graphql/mesh/spec/v1.0"
import: ["@grpcMethod", "@grpcConnectivityState", "@transport", "@extraSchemaDefinitionDirective"]
)
{
query: Query
}
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__field(
graph: join__Graph
requires: join__FieldSet
provides: join__FieldSet
type: String
external: Boolean
override: String
usedOverridden: Boolean
) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__implements(
graph: join__Graph!
interface: String!
) repeatable on OBJECT | INTERFACE
directive @join__type(
graph: join__Graph!
key: join__FieldSet
extension: Boolean! = false
resolvable: Boolean! = true
isInterfaceObject: Boolean! = false
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
scalar join__FieldSet
directive @link(
url: String
as: String
for: link__Purpose
import: [link__Import]
) repeatable on SCHEMA
scalar link__Import
enum link__Purpose {
"""
\`SECURITY\` features provide metadata necessary to securely resolve fields.
"""
SECURITY
"""
\`EXECUTION\` features provide metadata necessary for operation execution.
"""
EXECUTION
}
enum join__Graph {
PETS @join__graph(name: "Pets", url: "localhost:<Pets_port>")
STORES @join__graph(name: "Stores", url: "localhost:<Stores_port>")
}
directive @grpcMethod(
subgraph: String
rootJsonName: String
objPath: String
methodName: String
responseStream: Boolean
) repeatable on FIELD_DEFINITION
directive @grpcConnectivityState(subgraph: String, rootJsonName: String, objPath: String) repeatable on FIELD_DEFINITION
directive @transport(subgraph: String, kind: String, location: String, options: TransportOptions) repeatable on SCHEMA
directive @extraSchemaDefinitionDirective(directives: _DirectiveExtensions) repeatable on OBJECT
"""
Request to get all pet stores
"""
scalar pets__Empty_Input @join__type(graph: PETS) @specifiedBy(
url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"
)
scalar TransportOptions @join__type(graph: PETS) @join__type(graph: STORES)
scalar _DirectiveExtensions @join__type(graph: PETS) @join__type(graph: STORES)
"""
Request to get all pet stores
"""
scalar petstore__Empty_Input @join__type(graph: STORES) @specifiedBy(
url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"
)
type Query @extraSchemaDefinitionDirective(
directives: {transport: [{subgraph: "Pets", kind: "grpc", location: "localhost:<Pets_port>", options: {requestTimeout: 200000, roots: [{name: "Root0", rootJson: "{\\"options\\":{\\"syntax\\":\\"proto3\\"},\\"nested\\":{\\"pets\\":{\\"nested\\":{\\"Pet\\":{\\"fields\\":{\\"id\\":{\\"type\\":\\"int32\\",\\"id\\":1,\\"comment\\":null},\\"name\\":{\\"type\\":\\"string\\",\\"id\\":2,\\"comment\\":null}},\\"comment\\":\\"Message for pet store information\\"},\\"Empty\\":{\\"fields\\":{},\\"comment\\":\\"Request to get all pet stores\\"},\\"Pets\\":{\\"fields\\":{\\"pets\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"Pet\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":\\"Response with a list of pet stores\\"},\\"PetService\\":{\\"methods\\":{\\"GetAllPets\\":{\\"requestType\\":\\"Empty\\",\\"responseType\\":\\"Pets\\",\\"comment\\":null}},\\"comment\\":\\"Service definition for pet store\\"}}}}}"}]}}]}
) @extraSchemaDefinitionDirective(
directives: {transport: [{subgraph: "Stores", kind: "grpc", location: "localhost:<Stores_port>", options: {requestTimeout: 200000, roots: [{name: "Root0", rootJson: "{\\"options\\":{\\"syntax\\":\\"proto3\\"},\\"nested\\":{\\"petstore\\":{\\"nested\\":{\\"PetStore\\":{\\"fields\\":{\\"id\\":{\\"type\\":\\"int32\\",\\"id\\":1,\\"comment\\":null},\\"name\\":{\\"type\\":\\"string\\",\\"id\\":2,\\"comment\\":null},\\"location\\":{\\"type\\":\\"int32\\",\\"id\\":3,\\"comment\\":null},\\"petsForSale\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"int32\\",\\"id\\":4,\\"comment\\":null}},\\"comment\\":\\"Message for pet store information\\"},\\"Empty\\":{\\"fields\\":{},\\"comment\\":\\"Request to get all pet stores\\"},\\"PetStoreList\\":{\\"fields\\":{\\"petStores\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"PetStore\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":\\"Response with a list of pet stores\\"},\\"PetStoreService\\":{\\"methods\\":{\\"GetAllPetStores\\":{\\"requestType\\":\\"Empty\\",\\"responseType\\":\\"PetStoreList\\",\\"comment\\":null},\\"GetPetStorePets\\":{\\"requestType\\":\\"PetStore\\",\\"responseType\\":\\"PetStore\\",\\"comment\\":null}},\\"comment\\":\\"Service definition for pet store\\"}}}}}"}]}}]}
) @join__type(graph: PETS) @join__type(graph: STORES) {
pets_PetService_GetAllPets(input: pets__Empty_Input) : pets__Pets @grpcMethod(
subgraph: "Pets"
rootJsonName: "Root0"
objPath: "pets.PetService"
methodName: "GetAllPets"
responseStream: false
) @join__field(graph: PETS)
pets_PetService_connectivityState(tryToConnect: Boolean) : ConnectivityState @grpcConnectivityState(subgraph: "Pets", rootJsonName: "Root0", objPath: "pets.PetService") @join__field(graph: PETS)
petstore_PetStoreService_GetAllPetStores(input: petstore__Empty_Input) : petstore__PetStoreList @grpcMethod(
subgraph: "Stores"
rootJsonName: "Root0"
objPath: "petstore.PetStoreService"
methodName: "GetAllPetStores"
responseStream: false
) @join__field(graph: STORES)
petstore_PetStoreService_GetPetStorePets(input: petstore__PetStore_Input) : petstore__PetStore @grpcMethod(
subgraph: "Stores"
rootJsonName: "Root0"
objPath: "petstore.PetStoreService"
methodName: "GetPetStorePets"
responseStream: false
) @join__field(graph: STORES)
petstore_PetStoreService_connectivityState(tryToConnect: Boolean) : ConnectivityState @grpcConnectivityState(
subgraph: "Stores"
rootJsonName: "Root0"
objPath: "petstore.PetStoreService"
) @join__field(graph: STORES)
}
"""
Response with a list of pet stores
"""
type pets__Pets @join__type(graph: PETS) {
pets: [pets__Pet]
}
"""
Message for pet store information
"""
type pets__Pet @join__type(graph: PETS) {
id: Int
name: String
}
"""
Response with a list of pet stores
"""
type petstore__PetStoreList @join__type(graph: STORES) {
petStores: [petstore__PetStore]
}
"""
Message for pet store information
"""
type petstore__PetStore @join__type(graph: STORES) {
id: Int
name: String
location: Int
petsForSale: [Int]
}
enum ConnectivityState @join__type(graph: PETS) @join__type(graph: STORES) {
IDLE @join__enumValue(graph: PETS) @join__enumValue(graph: STORES)
CONNECTING @join__enumValue(graph: PETS) @join__enumValue(graph: STORES)
READY @join__enumValue(graph: PETS) @join__enumValue(graph: STORES)
TRANSIENT_FAILURE @join__enumValue(graph: PETS) @join__enumValue(graph: STORES)
SHUTDOWN @join__enumValue(graph: PETS) @join__enumValue(graph: STORES)
}
"""
Message for pet store information
"""
input petstore__PetStore_Input @join__type(graph: STORES) {
id: Int
name: String
location: Int
petsForSale: [Int]
}
"
`;

exports[`gRPC Multiple works: GetAllPetStores 1`] = `
{
"data": {
"petstore_PetStoreService_GetAllPetStores": {
"petStores": [
{
"id": 1,
"name": "Happy Paws Pet Store",
},
{
"id": 2,
"name": "Pet Paradise",
},
{
"id": 3,
"name": "Furry Friends",
},
{
"id": 4,
"name": "Paws and Claws",
},
{
"id": 5,
"name": "The Pet Shop",
},
],
},
},
}
`;

exports[`gRPC Multiple works: GetAllPets 1`] = `
{
"data": {
"pets_PetService_GetAllPets": {
"pets": [
{
"id": 1,
"name": "Pet1",
},
{
"id": 2,
"name": "Pet2",
},
{
"id": 3,
"name": "Pet3",
},
{
"id": 4,
"name": "Pet4",
},
{
"id": 5,
"name": "Pet5",
},
],
},
},
}
`;
54 changes: 54 additions & 0 deletions e2e/grpc-multiple/grpc-multiple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createTenv, type Serve } from '@e2e/tenv';

describe('gRPC Multiple', () => {
const { compose, service, serve } = createTenv(__dirname);
const queries = {
GetAllPets: /* GraphQL */ `
query GetAllPets {
pets_PetService_GetAllPets {
pets {
id
name
}
}
}
`,
GetAllPetStores: /* GraphQL */ `
query GetAllPetStores {
petstore_PetStoreService_GetAllPetStores {
petStores {
id
name
}
}
}
`,
};
let gw: Serve;
let supergraph: string;
beforeAll(async () => {
const { output, result } = await compose({
services: [await service('Pets'), await service('Stores')],
output: 'graphql',
});
supergraph = result;
gw = await serve({
supergraph: output,
});
});
it('composes', async () => {
const { result } = await compose({
services: [await service('Pets'), await service('Stores')],
maskServicePorts: true,
});
expect(result).toMatchSnapshot();
});
for (const queryName in queries) {
it('works', async () => {
const result = await gw.execute({
query: queries[queryName],
});
expect(result).toMatchSnapshot(queryName);
});
}
});
22 changes: 22 additions & 0 deletions e2e/grpc-multiple/mesh.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Opts } from '@e2e/opts';
import { defineConfig } from '@graphql-mesh/compose-cli';
import { loadGrpcSubgraph } from '@omnigraph/grpc';

const opts = Opts(process.argv);

export const composeConfig = defineConfig({
subgraphs: [
{
sourceHandler: loadGrpcSubgraph('Pets', {
endpoint: 'localhost:' + opts.getServicePort('Pets'),
source: './services/Pets/pets.proto', // only needed when not running reflection
}),
},
{
sourceHandler: loadGrpcSubgraph('Stores', {
endpoint: 'localhost:' + opts.getServicePort('Stores'),
source: './services/Stores/pet-store.proto', // only needed when not running reflection
}),
},
],
});
Loading

0 comments on commit c541164

Please sign in to comment.