playlyfe / go-graphql Goto Github PK
View Code? Open in Web Editor NEWA powerful GraphQL server implementation for Golang
A powerful GraphQL server implementation for Golang
Just made a go-graphql-relay starter kit that uses this graphql implementation. It provides an nice and simple "Hello world!" example. The starter kit works out of the box, just install the modules/packages and you're good to go ;)
It would be good to have some indication of what version of the GraphQL spec this implementation is currently aligned with. The current version is [email protected].
When returning enum values, the executor forces a coerceString(...)
on enums. However, the values do not always get translated to the proper string values. A quick fix for this is to modify the utils/coerce.go
file as follows:
utils/coerce.go
func coerceString(value interface{}) string {
...
default:
...
if v, ok := interface{}(value).(fmt.Stringer); ok {
return v.String(), true
}
return "", false
}
}
func coerceEnum(value interface{}) string {
...
default:
...
if v, ok := interface{}(value).(fmt.Stringer); ok {
return v.String(), true
}
return "", false
}
}
This way any enum defined with a Stringer method can properly send back the results.
Example:
package enums
const (
APPLE = Fruit(iota)
BANANA
STRAWBERRY
)
type Fruit int64
func (f Fruit) String() string {
switch f {
case APPLE:
return "APPLE"
case BANANA:
return "BANANA"
case STRAWBERRY:
return "STRAWBERRY"
}
return ""
}
@atrniv @pyros2097 Great work on this project. It's very different from the other 3 GraphQL libraries (as well as the reference JS implementation). For my purposes I think it is the most suitable base to work off. I am working on a massively multi-tenant project and need to design a GraphQL engine that can manage hundreds of schemas and alter them at runtime.
@paralin is doing some interesting work with Magellan. I will need to add features like that to the work I am doing as well. I don't want to fork and maintain an internal flavour of GraphQL -- as I realistically wouldn't be able to open source it
The other 3 GraphQL projects don't have a mechanism for us to contribute realistically (correct me if I am wrong @paralin).
I am hoping @atrniv @pyros2097 you guys can spend some resources to make it possible for us to contribute back, get parity with the spec and steward extensions which would allow us to build more advanced GraphQL engines.
I've been playing lately with this library and tested *graphql.ResolveParams to see what they offers. Since this library has no documentation, I'm looking to make one with basic and complex examples. For now to understant it how it works, I made a deep nested field that takes arguments for every sub-field and tried to implemented it. The code I ran looks like this:
package main
import (
"net/http"
"github.com/krypton97/GraphiQL"
"github.com/krypton97/HandleGraphQL"
"github.com/playlyfe/go-graphql"
)
var data = map[string]map[string]map[string]map[string]interface{}{
"CNU": {
"A": {
"13": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"14": {
"name": "ale",
"age": 13,
"id": "1",
"grade": 1,
},
"15": {
"name": "al",
"age": 19,
"id": "15",
"grade": 76,
},
"16": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 0,
},
},
"B": {
"1": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"2": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"3": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"4": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
},
"C": {
"412": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"413": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"415": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"416": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
},
},
"ETTI": {
"A": {
"73": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"74": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"75": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"76": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
},
"B": {
"23": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"34": {
"name": "tibi",
"age": 20,
"id": "1",
"grade": 1,
},
"25": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"56": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
},
"C": {
"63": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"54": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"45": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
"26": {
"name": "alex",
"age": 19,
"id": "1",
"grade": 1,
},
},
},
}
func main() {
schema := `
type Query {
students(school: String!): Class
}
type Class {
class(class: String!): Student
}
type Student {
student(id: ID!): Info
}
type Info {
name: String
age: Int
id: String
grade: Int
}
`
resolvers := map[string]interface{}{}
resolvers["Query/students"] = func(params *graphql.ResolveParams) (interface{}, error) {
return data[params.Args["school"].(string)], nil
}
resolvers["Class/class"] = func(params *graphql.ResolveParams) (interface{}, error) {
//fmt.Println(params.Source.(map[string]map[string]map[string]interface{})[params.Args["class"].(string)])
return params.Source.(map[string]map[string]map[string]interface{})[params.Args["class"].(string)], nil
}
resolvers["Student/student"] = func(params *graphql.ResolveParams) (interface{}, error) {
//fmt.Println(params.Source.(map[string]map[string]interface{})[params.Args["id"].(string)])
return params.Source.(map[string]map[string]interface{})[params.Args["id"].(string)], nil
}
executor, err := graphql.NewExecutor(schema, "Query", "", resolvers)
if err != nil {
panic(err)
}
api := handler.New(&handler.Config{
Executor: executor,
Context: "",
Pretty: true,
})
graphiql := graphiql.New("/graphql")
http.Handle("/graphql", api)
http.Handle("/", graphiql)
http.ListenAndServe(":3000", nil)
}
After some work, this handles all the cases(excepting when a case cannot be found and return nulll). What I was really looking for was to collect all the arguments from the query. After playing with the params, I found that they provide the source for the upper field so I could have simpled used the source and the current arguments the resolver uses to return what needs. Since I wasn't that sure I made a benchmark where I implemented the resolvers firstly with the params.Source method and secondly with already known arguments that matches the query, here's the benchmark to run http://pastebin.com/raw/yuFikjk5.
The results was extremly different:
Params.Source-8 20000 68259 ns/op
KnownsIDs-8 200000 11634 ns/op
My final question is: Is there a method to get the query arguments directly or the only way to work with nested fields with arguments is to use the params.Source?
I'm struggling with how to submit a query or mutation with an enum value as a parameter (e.g.).
query {
getReservations(days: [MONDAY, TUESDAY, WEDNESDAY]) {
id,
customer {
name,
phone
},
time
}
}
Is there any way this can be achieved? Any pointers and/or example(s) greatly appreciated!
Many many thanks!
Adding some descriptions to the types and fields exposed in this library would make it a lot more approachable. I am excited to start using it, but even understanding what is provided in the ResolveParams can require a lot of digging and reading code which doesn't need to be the case. I'd be happy to do some of this work but I am afraid that my understanding of the entire library is still limited so I am likely not the best person for the job.
I'm wondering if this library has a custom handler to set the endpoint. So far I've been using something like this
`http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
result, err := executor.Execute(context, r.URL.Query()["query"][0], variables, "")
if err != nil {
panic(err)
}
json.NewEncoder(w).Encode(result)
})`
,but I don't know how good will fit in production. If there's any handler that you're using guys would be nice add it to the library, ty :)
After playing with the resolvers, I've noticed a thing about them: they don't seem to care about higher sources. For example, the following resolverrs returns the same thing:
resolvers["Query/store"] = func(params *graphql.ResolveParams) (interface{}, error) {
return STORE, nil
}
resolvers["Store/teas"] = func(params *graphql.ResolveParams) (interface{}, error) {
return STORE["teas"], nil
}
and
resolvers["Query/store"] = func(params *graphql.ResolveParams) (interface{}, error) {
return map[string][]map[string]interface{}{}, nil //returning empty type
}
resolvers["Store/teas"] = func(params *graphql.ResolveParams) (interface{}, error) {
return STORE["teas"], nil
}
As you can see, it only cares about the deepest resolver. Is this a good or bad practice? Can I use it this way or I should return full source for every resolver?
I've been trying to figure it out for my own, but nothing seems to work since I can't find any documentation... I have the following code where I try to implement a nested type graphql. The schema is working fine, but it returns null..
package main
import (
"net/http"
"github.com/krypton97/HandleGraphQL"
"github.com/playlyfe/go-graphql"
)
var Salutations = map[string]string{
"ro": "Salut",
"en": "Hello",
"fr": "Oui",
"it": "Ciao",
"de": "Hallo",
}
var Students = map[string]map[string]interface{}{
"1": { "typename": "Student", "name": "alex", "age": 18, "id": "1", },
"2": { "typename": "Student", "name": "bob", "age": 19, "id": "2", },
"3": { "__typename": "Student", "name": "john", "age": 20, "id": "3", },
}
func main() {
schema := `
type StudentInfo {
name: String
age: Int
id: String
}
type Student{
student(id: String!) : StudentInfo
}
type QueryRoot {
hello: String
salutation(lang: String!): String
students: Student
}
`
resolvers := map[string]interface{}{}
resolvers["QueryRoot/hello"] = func(params *graphql.ResolveParams) (interface{}, error) {
return "world", nil
}
resolvers["QueryRoot/salutation"] = func(params *graphql.ResolveParams) (interface{}, error) {
return Salutations[params.Args["lang"].(string)], nil
}
resolvers["QueryRoot/students"] = func(params *graphql.ResolveParams) (interface{}, error) {
return Students, nil
}
resolvers["Student"] = func(params *graphql.ResolveParams) (interface{}, error) {
return Students[params.Args["id"].(string)], nil
}
executor, err := graphql.NewExecutor(schema, "QueryRoot", "", resolvers)
if err != nil {
panic(err)
}
api := handler.New(&handler.Config{
Executor: executor,
Context: "",
Pretty: true,
})
http.Handle("/graphql", api)
http.ListenAndServe(":3000", nil)
}
Looking to get this working out, but can't find out anything. I'd really appreciate if you guys could help me out :)
Currently, it is impossible to use schemas that use quotes to wrap descriptions.
It would be useful to update the lexer/parser code to properly handle quoted descriptions as per the current GraphQL specs.
Are there any plans to bring the code up to specs? Many thanks!
Is there any documentation?
Especially on using this with a router.
I was playing with the API and after a couple of tries I've noticed an issue with the returning value. I'll post here what my code looks like:
`
package main
import (
"net/http"
"fastQL/graphiql"
"github.com/krypton97/HandleGraphQL"
"github.com/playlyfe/go-graphql"
)
//Tea type
type Tea struct {
name string
steepingTime int
}
//Teas type
type Teas []*Tea
func main() {
schema := `
type Tea {
name: String
steepingTime: Int
}
type QueryRoot {
teas: [Tea]
}
`
resolvers := map[string]interface{}{}
resolvers["QueryRoot/teas"] = func(params *graphql.ResolveParams) (interface{}, error) {
return []map[string]interface{}{
{
"__typename": "Tea",
"name": "Earl Grey Blue Star",
"steepingTime": 5,
},
{
"__typename": "Tea",
"name": "Milk Oolong",
"steepingTime": 3,
},
{
"__typename": "Tea",
"name": "Gunpowder Golden Temple",
"steepingTIme": 2,
},
{
"__typename": "Tea",
"name": "Assam Hatimara",
"steepingTime": 5,
},
}, nil
}
executor, err := graphql.NewExecutor(schema, "QueryRoot", "", resolvers)
executor.ResolveType = func(value interface{}) string {
if object, ok := value.(map[string]interface{}); ok {
return object["__typename"].(string)
}
return ""
}
if err != nil {
panic(err)
}
api := handler.New(&handler.Config{
Executor: executor,
Context: "",
Pretty: true,
})
graphiql := graphiql.New("/graphql")
http.Handle("/graphql", api)
http.Handle("/query", graphiql)
http.ListenAndServe(":3000", nil)
}
`
Running the following code return the following:
`
{
"data": {
"teas": [
{
"name": "Earl Grey Blue Star",
"steepingTime": 5
},
{
"name": "Milk Oolong",
"steepingTime": 3
},
{
"name": "Gunpowder Golden Temple",
"steepingTime": null
},
{
"name": "Assam Hatimara",
"steepingTime": 5
}
]
}
}
`
As you can see, the third field returns a null value, but the rest of them are ok. I can't seem to figure it out, any reason for that?
Looks like ints are being casted as int32:
Line 728 in 566b9f5
A few lines above, ints are parsed as 64-bit though:
Line 723 in 566b9f5
Why not leave everything as 64-bit? I am using this library and got an overflow of an int field that holds milliseconds since the epoch (Unix time * 1000), since that is bigger than 2^32
https://github.com/playlyfe/go-graphql/blob/master/executor.go#L1043
Should be easy enough to use a channel for results and goroutines to launch each sub-resolver on an object.
I've been looking for some examples, but couldn't find anything. Since the docs lacks a bit, I was wondering if it could be used with react-relay. An workaround would be using the apollo-client which is compatible with every backend language, but is not as great as relay is...If there's anyone that managed to do so, I'd be gladful for any source code ;)
Hi,
I really like your implementation that defines gql in text! I am hoping to use the package in our project but it's not clear to me what's the status of this package. Could you put the status in Readme.MD? e.g. It's production ready, beta, alpha or WIP, graphQL spec supported version?
Thanks!
Ming
Some newer apps complain about missing locations when placing an IntrospectionQuery request.
A quick fix is to add the following to the directive's definition object in the schema.go file:
"locations": []string{"FIELD", "FRAGMENT_DEFINITION", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"},
Below is an example where it would be placed...
schema.go (lines 269-315)
result := map[string]interface{}{
"queryType": executor.introspectType(params, queryRoot),
"directives": []map[string]interface{}{
{
"name": "skip",
"description": "Conditionally exclude a field or fragment during execution",
"args": []map[string]interface{}{ ... },
"locations": []string{"FIELD", "FRAGMENT_DEFINITION", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"},
"onOperation": false,
"onField": true,
"onFragment": true,
},
...
},
}
schema.go (lines 88-96)
const INTROSPECTION_SCHEMA = `
...
type __Directive {
name: String!
description: String
args: [__InputValue!]!
locations: [String!]!
onOperation: Boolean!
onFragment: Boolean!
onField: Boolean!
}
...
`
Currently a date is outputted using the default date format, which isn't parseable by javascript. Since javascript is a language wherein graphql is often interacted, with this is an issue IMO.
I worked around it by adding a custom time type which implements String()
with the right date format.
type JsTime struct {
time.Time
}
func (t JsTime) String() string {
return t.Time.Format(time.RFC3339)
}
But optimally there should be a similar interface like the json one, with UnmarshalJSON
and MarshalJSON
.
Its seems the graphql-go guys tested their library for performance and improved it by a factor of 30 by using bytes instead of string. So I checked the source and even we are using string which causes a lot of garbage collection it seems so I guess we also need to make the change later to seem how much perf we gain.
References:
graphql-go/graphql@065ab6b
graphql-go/graphql#119
We should take a look at this lib for improving the lexer,
https://github.com/tdewolff/buffer
Looking to implement pagination with this library and I found a strange bug. By strange I mean that the fields are recognised, but the value is not returned. This is the code I have:
package data
import "github.com/playlyfe/go-graphql"
import "fmt"
var courses = map[string]map[string][]map[string]interface{}{
"linkConnection": {
"edges": {
{
"cursor": "YXJyYXljb25uZWN0aW9uOjA=",
"node": map[string]string{
"id": "507f191e810c19729de860ea",
"title": "RelayJS course",
"url": "https://relay.cool.com/",
"createdAt": "1974-08-07T11:53:39.056Z",
},
},
{
"cursor": "YXJyYXljb25uZWN0aW9uOjE=",
"node": map[string]string{
"id": "58bc29705d58db311b3d1d85",
"title": "test link",
"url": "test.com",
"createdAt": "1974-08-07T11:53:39.055Z",
},
},
{
"cursor": "YXJyYXljb25uZWN0aW9uOjI=",
"node": map[string]string{
"id": "507f1f77bcf86cd799439011",
"title": "ReactJS main course",
"url": "https://mlab.com/databases/mongo/",
"createdAt": "1974-08-07T11:53:39.054Z",
},
},
},
},
}
var Schema = `
type Query {
node(id: ID!): Node
store: Store
}
type Mutation {
createLink(input: CreateLinkInput!): CreateLinkPayload
}
interface Node {
id: ID!
}
type Store implements Node {
id: ID!
linkConnection(after: String, first: Int, before: String, last: Int, query: String): LinkConnection
}
input CreateLinkInput {
title: String!
url: String!
clientMutationId: String
}
type CreateLinkPayload {
linkEdge: LinkEdge
store: Store
clientMutationId: String
}
type Link {
id: ID!
title: String
url: String
createdAt: String
}
type LinkConnection {
pageInfo: PageInfo!
edges: [LinkEdge]
}
type LinkEdge {
node: Link
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
`
var Resolvers = map[string]interface{}{
"Query/store": func(p *graphql.ResolveParams) (interface{}, error) {
return courses, nil
},
"Store/linkConnection": func(p *graphql.ResolveParams) (interface{}, error) {
return courses["linkConnection"], nil
},
"LinkConnection/edges": func(p *graphql.ResolveParams) (interface{}, error) {
fmt.Println(courses["linkConnection"]["edges"][0]["node"])
return courses["linkConnection"]["edges"], nil
},
}
and the output on graphiql looks like this
{
"data": {
"store": {
"linkConnection": {
"edges": [
{
"cursor": "YXJyYXljb25uZWN0aW9uOjA=",
"node": {
"createdAt": null,
"title": null,
"url": null
}
},
{
"cursor": "YXJyYXljb25uZWN0aW9uOjE=",
"node": {
"createdAt": null,
"title": null,
"url": null
}
},
{
"cursor": "YXJyYXljb25uZWN0aW9uOjI=",
"node": {
"createdAt": null,
"title": null,
"url": null
}
}
]
}
}
}
}
I might be doind it wrong, but since the fields are recognised, it makes me think about it..
Could you share and example using the pkg with net/http
I'm using the library now in my code and quite like how robust it is.
Can we add subscriptions? They are supported in the GraphQL spec and Apollo client.
https://dev-blog.apollodata.com/graphql-subscriptions-in-apollo-client-9a2457f015fb#.lkud9zs7b
subscription comments($repoName: String!) {
newComments(repoName: $repoName) {
content
postedBy {
username
}
postedAt
}
}
From the article:
Based on the subscription field name “newComments” and a mapping from subscription names to channels, the server subscribes to one or more pub/sub channels. When something is published to one of these channels, the server runs the GraphQL query specified in the subscription and sends a full new result to the client.
My API surface idea for this is:
net.Context
argument to the resolve parameters. Use a context for each request. Similar to how go-grpc
does it.chan interface{}
channel.It's a little tough to work around the current pattern of Execute
-> return result. I think the best way to handle this is to change the Execute
function OR add another function that returns an object instead of a map[string]interface{}
, and also accepts a root Context
. In this object we can define how the result is shown. If it's a single map[string]interface{}
result, set the result
field of that object. Otherwise, set the resultChan
fields on that object.
Was wondering if there is any appetite for enhancing the resolver api by enabling dynamic argument parsing for faster/safer coding.
Below is a rough (but working) example of how it could work...aided by some minimal external packages I quickly pulled together...
Any comments? Takers?
package main
import (
"errors"
"net/http"
handler "github.com/krypton97/HandleGraphQL"
router "github.com/luisjakon/playlyfe-router"
graphiql "github.com/luisjakon/graphiql"
graphql "github.com/playlyfe/go-graphql"
)
var (
Salutations = map[string]string{
"ro": "Salut",
"en": "Hello",
"fr": "Oui",
"it": "Ciao",
"de": "Hallo",
}
Students = map[string]map[string]interface{}{
"1": {"__typename": "Student", "name": "alex", "age": 18, "id": "1"},
"2": {"__typename": "Student", "name": "bob", "age": 19, "id": "2"},
"3": {"__typename": "Student", "name": "john", "age": 20, "id": "3"},
}
)
func main() {
schema := `
type Student {
name: String
age: Int
}
type Query {
hello: String
salutation(lang: String!): String
student(id: String!): Student
}
`
router := router.NewRouter()
router.Register("Query/hello", QueryResolver.Hello)
router.Register("Query/salutation", QueryResolver.Salutation)
router.Register("Query/student", QueryResolver.StudentById)
router.Register("Student/age", StudentResolver.Age)
executor, err := graphql.NewExecutor(schema, "Query", "", router)
if err != nil {
panic(err)
}
api := handler.New(&handler.Config{
Executor: executor,
Context: "",
Pretty: true,
})
http.Handle("/graphql", graphiql.Handler(api))
http.ListenAndServe("localhost:3000", nil)
}
resolvers.go
// Resolvers
var (
QueryResolver = queryRes{}
StudentResolver = studentRes{}
)
// Query Resolver
type queryRes struct{}
func (r queryRes) Hello(params *graphql.ResolveParams) (interface{}, error) {
return "world", nil
}
func (r queryRes) Salutation(params *graphql.ResolveParams, args struct { Lang string }) (interface{}, error) {
s := Salutations[args.Lang]
if s == "" {
return nil, errors.New("Unknown Language: " + args.Lang)
}
return s, nil
}
func (r queryRes) StudentById(params *graphql.ResolveParams, args struct { Id string }) (interface{}, error) {
return Students[args.Id], nil
}
// Student Resolver
type studentRes struct{}
func (r studentRes) Age(params *graphql.ResolveParams) (interface{}, error) {
if v, ok := params.Source.(int); ok {
return v, nil
}
source := params.Source.(map[string]interface{})
id := source["id"].(string)
return Students[id]["age"].(int), nil
}
It would be useful to pass a x/net/context.Context
with Execute
to allow canceling processing a query before it is done. This is especially useful for upcoming "Subscriptions"
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.