-
Notifications
You must be signed in to change notification settings - Fork 348
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix multiple gRPC subgraphs in a supergraph (#8203)
* Fix multiple gRPC subgraphs in a supergraph * Snapshots
- Loading branch information
Showing
18 changed files
with
649 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
267 changes: 267 additions & 0 deletions
267
e2e/grpc-multiple/__snapshots__/grpc-multiple.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
], | ||
}, | ||
}, | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}), | ||
}, | ||
], | ||
}); |
Oops, something went wrong.