Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix multiple gRPC subgraphs in a supergraph #8203

Merged
merged 2 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading