A directive-driven runtime and schema validator for gRPC-backed GraphQL APIs.
This project is experimental and is not a fully-supported Apollo Graph project. We may not respond to issues and pull requests at this time. See Known Limitations.
Generate a GraphQL schema from a Protocol Buffer IDL. The result is most likely not the GraphQL API you want, but it's a good starting point!
npx github:apollosolutions/schema-driven-grpc generate --proto file.proto \
--service com.example.ServiceName \
--name SERVICE \
--address localhost:50051
Validate a schema against a Protocol Buffer IDL, ensuring that the runtime behavior will work against a gRPC API.
npx github:apollosolutions/schema-driven-grpc validate --schema schema.graphql --federated
(You can also pass --watch
to continuously validate your schema during development.)
Serve a GraphQL API in front of a gRPC API.
npx github:apollosolutions/schema-driven-grpc serve --schema schema.graphql \
--federated \
--port 4000
docker run \
-e FEDERATED=true \ # optional
-v $PWD:/etc/config \ # directory with your schema.graphql and .proto files
-p 4000:4000 \
ghcr.io/apollosolutions/schema-driven-grpc
Apply this directive to values in an enum in order to define:
- The location of the
.proto
files (relative to this GraphQL schema file). - The fully-qualified service name.
- The gRPC service address.
- Any client metadata (i.e. headers).
enum grpc__Service {
MY_SERVICE
@grpc(
protoFile: "path/relative/to/schema/file/service.proto"
serviceName: "com.example.MyService"
address: "localhost:50051"
metadata: [
{ name: "authorization", valueFrom: "req.headers.authorization" }
{ name: "headername", value: "staticvalue" }
]
)
}
Add the fetch directive to any field on any object type, including the Query and Mutation types. Reference a service with its annotated enum value.
type Query {
post(id: ID!): Post! @grpc__fetch(service: MY_SERVICE, rpc: "GetPost")
}
type Post {
id: ID!
comments: [Comment]
# you'll most likely need the mapArguments and dig arguments; see below
@grpc__fetch(service: MY_SERVICE, rpc: "GetCommentsForPost")
}
It's common to use a response message in gRPC, while in GraphQL you may not want
that extra layer. Use the dig
argument to "dig" a value out of the response
message.
service MyService {
rpc GetPost (GetPostRequest) returns (GetPostResponse) {}
}
message GetPostRequest {
string id = 1;
}
message GetPostResponse {
Post post = 1;
}
type Query {
post(id: ID!): Post!
@grpc__fetch(service: MY_SERVICE, rpc: "GetPost", dig: "post")
}
Use the mapArguments
argument to pluck values off the parent object for use
as fields on the request message. In this example, the Post
Protocol Buffer
message has an author_id
field (which we don't expose in GraphQL). We can
pass that value as the id
field of the GetPerson
request message.
type Post {
id: ID!
author: Person
@grpc__fetch(
service: MY_SERVICE
rpc: "GetPerson"
mapArguments: { sourceField: "author_id", arg: "id" }
)
}
To efficiently batch RPCs and avoid the N+1 query problem, use the dataloader
argument. You can specify the cache key, the RPC request message field for the
list of cache keys, and the field that must match the cache key to correctly
store the response message for the lifetime of the request.
type Post {
id: ID!
author: Person
@grpc__fetch(
service: MY_SERVICE
rpc: "BatchGetPerson"
dataloader: {
key: "$source.author_id" # value on parent message
listArgument: "ids" # field on the request type
responseKey: "id" # field on the items in the response type that must match the key
}
)
}
The dataloader.key
argument can come from the parent message
($source.author_id
) or from the field arguments ($args.id
). The latter is
used mostly in Entity Resolvers, but you can also use it
in other fields if you want to aggressively cache-and-batch RPCs.
type Query {
post(id: ID!): Post
@grpc__fetch(
service: POSTS
rpc: "BatchGetPosts"
dig: "posts"
dataloader: { key: "$args.id", listArgument: "ids", responseKey: "id" }
)
}
type Post {
id: ID
title: String
}
When using Apollo Federation, you can add the fetch directive to object types as
well to create the equivalent of the __resolveReference
resolver.
type Post
@key(fields: "id")
@grpc__fetch(
service: MY_SERVICE
rpc: "BatchGetPosts"
dig: "posts"
dataloader: { key: "$args.id", listArgument: "ids", responseKey: "id" }
) {
id: ID!
title: String
}
In this example, $args
refers to the entity representation passed to
__resolveReference
.
The renamed directive allows renaming elements from your gRPC API. You can rename field arguments:
type Query {
product(id: ID! @grpc__renamed(from: "sku")): Product
@grpc__fetch(service: PRODUCTS, rpc: "GetProduct")
}
Fields:
type Product {
price: Int @grpc__renamed(from: "amount")
}
Enum values:
enum Status {
AVAILABLE
UNAVAILABLE @grpc__renamed(from: "NOT_AVAILABLE")
}
Takes a value off the Protocol Buffer message and wraps it in separate GraphQL type. The primary use-case is converting foreign keys into nested objects (especially useful in federation if the nested object is an entity provided by another service.)
message Post {
string id = 1;
string title = 2;
string author_id = 3;
}
type Post {
id: ID!
title: String
author: Person @grpc__wrap(gql: "id", proto: "author_id")
}
type Person @key(fields: "id") @extends {
id: ID! @external
}
GraphQL and gRPC differ in their approach to nullability.
- In GraphQL, nullability is a first-class feature of the schema definition language.
- gRPC's wire protocol (Protocol Buffers) do not differentiate between "zero values" and omitted values. On the wire you can't tell if a string field was omitted from the response or is a zero-length string.
With this in mind, this project takes the stance that no scalar values are
nullable. You're free to add !
to your GraphQL schema for scalar, enum, and
list fields — they will never be null.
Field Type | Zero Value |
---|---|
String | "" |
Int | 0 |
Float | 0 |
Boolean | false |
ID | "" |
enum | First value of the Protobuf enum |
list | [] |
Fields that return messages types can be null, and there's no built-in way to
determine nullability from .proto
files, so the safest choice is to never use
a !
on a GraphQL field that returns a composite type.
npm i github:apollosolutions/schema-driven-grpc#v0.1.0
This library exports two functions for loading the schema and providing the resolver function:
import { load, makeFieldResolver } from "@apollosolutions/schema-driven-grpc";
const { schema, services } = load("path/to/schema.graphql", {
federated: true,
});
const server = new ApolloServer({
schema,
fieldResolver: makeFieldResolver(services),
plugins: [ApolloServerPluginInlineTraceDisabled()],
context(args) {
return args; // used for metadata
},
});
server
.listen(4000)
.then(({ url }) => console.log(`Running GraphQL API at ${url}`));
The grpc
directives and types should be stripped from your API schema so as
not to expose implementation details to API consumers. Also, types like the
grpc__Service
enum will conflict with other schemas if you are composing
multiple schema-driven-grpc services together using Apollo Federation.
Before publishing a schema-driven-grpc API to Apollo Studio or another schema
registry, run the make-api-schema
command from this package:
schema-driven-grpc make-api-schema \
--schema schema-with-grpc-directives.graphql \
--federated > api-schema.graphql
- gRPC client credentials are currently hardcoded to
createInsecure
(appropriate when you're using a service mesh that supports mutual TLS, but otherwise not a good idea). - Will not support protocol buffer Maps.
- Does not yet support protocol buffer
oneof
. - Supports only the
proto3
syntax. - Does not validate non-nullability/
required
. - To use the
metadata
argument in@grpc
, you must create a GraphQL context object for each request that maps to yourvalueFrom
arguments. - Support for protocol buffer
bytes
field types is buggy. - Does not support Apollo Federation subgraph introspection (the
{ _service { sdl } }
operation). - Does not support nested
@key
directives when usingmapArguments
with entities.
- Find all fields and entity types marked with
@grpc__fetch
. These are the "fetch roots". - From each fetch root, walk the graph and compare GraphQL fields against the
relevant Protocol Buffer message, starting with the RPC response type.
Because RPC APIs are not recursive, all paths must terminate.
- A path terminates at scalar or enum fields, or at another fetch root.
- If type recursion is encountered, the field must be nullable and we can end a path here as well.
- While walking the graph, each time we encounter a fetch root, record the
protobuf type corresponding with the parent of the fetch root. We'll use
that type for validating arguments plucked off parents with
mapArguments: { sourceField: }
.
- At each node in the graph, check that field names and type match (taking rename directives into account).
- Validate that the field arguments on a fetch root match the fields on the RPC request type.
- Create a
grpc__Service
enum value for the service. - Create a Mutation field for each RPC (there's no way to differentiate between
side-effectful and side-effectless RPCs, so we must assume they're all
mutations). Add a
@grpc__fetch
directive on each. - Use RPC request types to create field arguments.
- Recursively create types and enums starting with RPC response types.
- Additional validation rules:
- Root fields must have a fetch directive.
- Wrap and Rename directives can't exist on the same field.
- Federation:
@key
d entities must have a@grpc__fetch
directive. - Disallow unreachable fields?
- Support error unions?
- Subscriptions with gRPC streams?