GraphQL API
Gateway
Go code generation isn’t frightful.
Yaroslav Mytso
Software engineer at EGT Ukraine
Service 1
(GRPC API)
Service 2
(GRPC API)
Service 3
(GRPC API)
Service N
(GRPC API)
Front-end
X
X
X
X
API
Gateway
Service 1
(GRPC API)
Service 2
(GRPC API)
Service 3
(GRPC API)
Service N
(GRPC API)
Front-end
{
user(id: «1») {
name
}
dog(name: «A»){
nick
}
}
{
«user»: {
«name»: «UserName»
},
«dog»: {
«nick»: «Bobby»
}
}
Type: User
Type: Pet
Type: String
Type: String
Type: Query
GraphQL → GRPC Proxy writing process
GRPC Message
GraphQL
Output Object
GraphQL
Input Object
GRPC Service
GraphQL
Fields Array
GRPC Enum
GraphQL
Enum
Input Object
unmarshaler+
GRPC
Method
Method
Resolver
Message in .proto file VS Message in GraphQL Schema
message A {
int32 someField = 1;
int64 someAnotherField = 2;
string andAnotherOne = 3;
}
var GQLA = graphql.NewObject(graphql.ObjectConfig{
Name: "A",
Fields: graphql.Fields{
"someField": &graphql.Field{
Name: "someField",
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
switch src := p.Source.(type) {
case A:
return src.SomeField, nil
case *A:
return src.SomeField, nil
}
return nil, nil
},
},
"someAnotherField": &graphql.Field{
Name: "someAnotherField",
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
switch src := p.Source.(type) {
case A:
return src.SomeAnotherField, nil
case *A:
return src.SomeAnotherField, nil
}
return nil, nil
},
},
"andAnotherOne": &graphql.Field{
Name: "andAnotherOne",
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
switch src := p.Source.(type) {
case A:
return src.AndAnotherOne, nil
case *A:
return src.AndAnotherOne, nil
}
return nil, nil
},
},
},
})
😞
protoc-gen-gogoopsee
(gogo plugin)
http://github.com/opsee/protobuf/
*.proto gogo
stdin
stdout
GRPC Client
GRPC Client Tests
GoGo code generation process
plugin 1plugin 1 plugin 2 plugin N
protoc-gen-gogoopsee
GRPC Message
GraphQL
Output Object
GraphQL
Input Object
GRPC Service
GraphQL
Fields Array
GRPC Enum
GraphQL
Enum
Input Object
unmarshaler+
GRPC
Method
Field
Resolver
X
X
X
p.P(`»`, field.GetName(), `": &`, graphQLPkg.Use(), `.Field{`)
p.In()
p.P(`Type: `, p.graphQLType(message, field, graphQLPkg, schemaPkg), `,`)
p.P(`Description: `, fieldGQL, `,`)
p.P(`Resolve: func(p `, graphQLPkg.Use(), `.ResolveParams) (interface{}, error) {`)
p.In()
p.P(`return nil, nil`)
p.Out()
p.P(`}}`)
p.Out()
fmt.Println() generated code
«{{$field.Name}}»: {{gqlPkg}}.Field{
Type: {{call $field.Type}},
Description: {{$field.Description}},
Resolve: func(p {{gqlPkg}}.ResolveParams) (interface{}, error) {
return nil, nil
}
}
Generate code using
text/template
GoGo plugin problems
• Only two files in output
• Generator knows about only one .proto file and can’t
make composition
• Hard to debug
• Option-based configuration
.proto files parser
generate.yaml
vendor_path: «./vendor»
paths:
- «$GOPATH/src»
- «./vendor»
proto_files:
- path : «./a.proto»
- …
- path: «./b.proto»
schema:
Query:
- field: «a»
type: «SERVICE»
- field: «b»
type: «SERVICE»
Parser
Query {
a : AService,
b: BService,
}
AService {
meth1(req:Req) : MethResp,
meth2(req:Req) : SomeRes,
}
BService {
meth1(req:Req) : MethResp,
}
a.proto b.proto
Template
schema.go
./services/a.go
./services/b.go
proto2gql
Too heavy template
Generator responsibilities
.proto files
GraphQL schema
Generator responsibilities
.proto files
GraphQL schema
swagger files
Custom .proto files parser
generate.yaml
vendor_path: «./vendor»
paths:
- «$GOPATH/src»
- «./vendor»
proto_files:
- path : «./a.proto»
- …
- path: «./b.proto»
schema:
Query:
- field: «a»
type: «SERVICE»
- field: «b»
type: «SERVICE»
Query {
a : AService,
b: BService,
}
AService {
meth1(req:Req) : MethResp,
meth2(req:Req) : SomeRes,
}
BService {
meth1(req:Req) : MethResp,
}
Parser
a.proto b.proto
Template
schema.go
./services/a.go
./services/b.go
Normalizing data
generate.yaml
vendor_path: «./vendor»
paths:
- «$GOPATH/src»
- «./vendor»
proto_files:
- path : «./a.proto»
- …
- path: «./b.proto»
schema:
Query:
- field: «a»
type: «SERVICE»
- field: «b»
type: «SERVICE»
Query {
a : AService,
b: BService,
}
AService {
meth1(req:Req) : MethResp,
meth2(req:Req) : SomeRes,
}
BService {
meth1(req:Req) : MethResp,
}
.proto
parser
a.proto
b.proto
Template
schema.go
./services/a.go
./services/b.go
swagger
Parser
data
normalizer
data
normalizer
swagger1.yml
swagger2.yml
generate.yaml
vendor_path: «./»
graphql:
schemas:
…
proto2gql:
proto_files:
- …
swagger2gql:
swagger_files:
- …
graphql plugin
(knows how to generate graphql
schema based on own DTO’s)
proto2gql plugin
(knows how to convert info about
*.proto file into graphql DTO’s)
proto2gql
swagger2gql plugin
(knows how to convert info
swagger file into graphql DTO’s)
Plugins
github.com/saturn4er/proto2gql
Conclusions
• Use templates in code generation
• Prepare data for templates in case of a possibility
of some other data-source
• Choose toolchain that will not frame you
Questions?
Yaroslav Mytso
yaroslav.mitso@egt-ua.com
github.com/saturn4er

Graph ql api gateway

  • 1.
    GraphQL API Gateway Go codegeneration isn’t frightful. Yaroslav Mytso Software engineer at EGT Ukraine
  • 2.
    Service 1 (GRPC API) Service2 (GRPC API) Service 3 (GRPC API) Service N (GRPC API) Front-end X X X X
  • 3.
    API Gateway Service 1 (GRPC API) Service2 (GRPC API) Service 3 (GRPC API) Service N (GRPC API) Front-end
  • 4.
    { user(id: «1») { name } dog(name:«A»){ nick } } { «user»: { «name»: «UserName» }, «dog»: { «nick»: «Bobby» } } Type: User Type: Pet Type: String Type: String Type: Query
  • 5.
    GraphQL → GRPCProxy writing process GRPC Message GraphQL Output Object GraphQL Input Object GRPC Service GraphQL Fields Array GRPC Enum GraphQL Enum Input Object unmarshaler+ GRPC Method Method Resolver
  • 6.
    Message in .protofile VS Message in GraphQL Schema message A { int32 someField = 1; int64 someAnotherField = 2; string andAnotherOne = 3; } var GQLA = graphql.NewObject(graphql.ObjectConfig{ Name: "A", Fields: graphql.Fields{ "someField": &graphql.Field{ Name: "someField", Type: graphql.Int, Resolve: func(p graphql.ResolveParams) (interface{}, error) { switch src := p.Source.(type) { case A: return src.SomeField, nil case *A: return src.SomeField, nil } return nil, nil }, }, "someAnotherField": &graphql.Field{ Name: "someAnotherField", Type: graphql.Int, Resolve: func(p graphql.ResolveParams) (interface{}, error) { switch src := p.Source.(type) { case A: return src.SomeAnotherField, nil case *A: return src.SomeAnotherField, nil } return nil, nil }, }, "andAnotherOne": &graphql.Field{ Name: "andAnotherOne", Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { switch src := p.Source.(type) { case A: return src.AndAnotherOne, nil case *A: return src.AndAnotherOne, nil } return nil, nil }, }, }, }) 😞
  • 7.
  • 8.
    *.proto gogo stdin stdout GRPC Client GRPCClient Tests GoGo code generation process plugin 1plugin 1 plugin 2 plugin N
  • 9.
    protoc-gen-gogoopsee GRPC Message GraphQL Output Object GraphQL InputObject GRPC Service GraphQL Fields Array GRPC Enum GraphQL Enum Input Object unmarshaler+ GRPC Method Field Resolver X X X
  • 10.
    p.P(`»`, field.GetName(), `":&`, graphQLPkg.Use(), `.Field{`) p.In() p.P(`Type: `, p.graphQLType(message, field, graphQLPkg, schemaPkg), `,`) p.P(`Description: `, fieldGQL, `,`) p.P(`Resolve: func(p `, graphQLPkg.Use(), `.ResolveParams) (interface{}, error) {`) p.In() p.P(`return nil, nil`) p.Out() p.P(`}}`) p.Out() fmt.Println() generated code
  • 11.
    «{{$field.Name}}»: {{gqlPkg}}.Field{ Type: {{call$field.Type}}, Description: {{$field.Description}}, Resolve: func(p {{gqlPkg}}.ResolveParams) (interface{}, error) { return nil, nil } } Generate code using text/template
  • 12.
    GoGo plugin problems •Only two files in output • Generator knows about only one .proto file and can’t make composition • Hard to debug • Option-based configuration
  • 13.
    .proto files parser generate.yaml vendor_path:«./vendor» paths: - «$GOPATH/src» - «./vendor» proto_files: - path : «./a.proto» - … - path: «./b.proto» schema: Query: - field: «a» type: «SERVICE» - field: «b» type: «SERVICE» Parser Query { a : AService, b: BService, } AService { meth1(req:Req) : MethResp, meth2(req:Req) : SomeRes, } BService { meth1(req:Req) : MethResp, } a.proto b.proto Template schema.go ./services/a.go ./services/b.go proto2gql
  • 14.
  • 15.
  • 16.
  • 17.
    Custom .proto filesparser generate.yaml vendor_path: «./vendor» paths: - «$GOPATH/src» - «./vendor» proto_files: - path : «./a.proto» - … - path: «./b.proto» schema: Query: - field: «a» type: «SERVICE» - field: «b» type: «SERVICE» Query { a : AService, b: BService, } AService { meth1(req:Req) : MethResp, meth2(req:Req) : SomeRes, } BService { meth1(req:Req) : MethResp, } Parser a.proto b.proto Template schema.go ./services/a.go ./services/b.go
  • 18.
    Normalizing data generate.yaml vendor_path: «./vendor» paths: -«$GOPATH/src» - «./vendor» proto_files: - path : «./a.proto» - … - path: «./b.proto» schema: Query: - field: «a» type: «SERVICE» - field: «b» type: «SERVICE» Query { a : AService, b: BService, } AService { meth1(req:Req) : MethResp, meth2(req:Req) : SomeRes, } BService { meth1(req:Req) : MethResp, } .proto parser a.proto b.proto Template schema.go ./services/a.go ./services/b.go swagger Parser data normalizer data normalizer swagger1.yml swagger2.yml
  • 19.
    generate.yaml vendor_path: «./» graphql: schemas: … proto2gql: proto_files: - … swagger2gql: swagger_files: -… graphql plugin (knows how to generate graphql schema based on own DTO’s) proto2gql plugin (knows how to convert info about *.proto file into graphql DTO’s) proto2gql swagger2gql plugin (knows how to convert info swagger file into graphql DTO’s) Plugins
  • 20.
  • 21.
    Conclusions • Use templatesin code generation • Prepare data for templates in case of a possibility of some other data-source • Choose toolchain that will not frame you
  • 22.