Skip to content

Commit

Permalink
Merge pull request #2 from tokopedia/fork_syncup_with_master_Aug19
Browse files Browse the repository at this point in the history
Fork syncup with master aug19
  • Loading branch information
Abhishek Srivastava authored Sep 4, 2019
2 parents b9dc447 + 3e31d0e commit 04acea9
Show file tree
Hide file tree
Showing 39 changed files with 7,513 additions and 533 deletions.
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Contributing

- With issues:
- Use the search tool before opening a new issue.
- Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them.

- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integrations systems such as TravisCI.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
25 changes: 0 additions & 25 deletions Gopkg.lock

This file was deleted.

10 changes: 0 additions & 10 deletions Gopkg.toml

This file was deleted.

16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ safe for production use.
- resolvers are matched to the schema based on method sets (can resolve a GraphQL schema with a Go interface or Go struct).
- handles panics in resolvers
- parallel execution of resolvers
- subscriptions
- [sample WS transport](https://github.com/graph-gophers/graphql-transport-ws)

## Roadmap

Expand Down Expand Up @@ -63,7 +65,17 @@ $ curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query

### Resolvers

A resolver must have one method for each field of the GraphQL type it resolves. The method name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the field's name in a non-case-sensitive way.
A resolver must have one method or field for each field of the GraphQL type it resolves. The method or field name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the schema's field's name in a non-case-sensitive way.
You can use struct fields as resolvers by using `SchemaOpt: UseFieldResolvers()`. For example,
```
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
schema := graphql.MustParseSchema(s, &query{}, opts...)
```

When using `UseFieldResolvers` schema option, a struct field will be used *only* when:
- there is no method for a struct field
- a struct field does not implement an interface method
- a struct field does not have arguments

The method has up to two arguments:

Expand Down Expand Up @@ -97,4 +109,4 @@ func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {

[deltaskelta/graphql-go-pets-example](https://github.com/deltaskelta/graphql-go-pets-example) - graphql-go resolving against a sqlite database

[OscarYuen/go-graphql-starter](https://github.com/OscarYuen/go-graphql-starter) - a starter application integrated with dataloader, psql and basic authenication
[OscarYuen/go-graphql-starter](https://github.com/OscarYuen/go-graphql-starter) - a starter application integrated with dataloader, psql and basic authentication
98 changes: 98 additions & 0 deletions example/caching/cache/hint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Package cache implements caching of GraphQL requests by allowing resolvers to provide hints about their cacheability,
// which can be used by the transport handlers (e.g. HTTP) to provide caching indicators in the response.
package cache

import (
"context"
"fmt"
"time"
)

type ctxKey string

const (
hintsKey ctxKey = "hints"
)

type scope int

// Cache control scopes.
const (
ScopePublic scope = iota
ScopePrivate
)

const (
hintsBuffer = 20
)

// Hint defines a hint as to how long something should be cached for.
type Hint struct {
MaxAge *time.Duration
Scope scope
}

// String resolves the HTTP Cache-Control value of the Hint.
func (h Hint) String() string {
var s string
switch h.Scope {
case ScopePublic:
s = "public"
case ScopePrivate:
s = "private"
}
return fmt.Sprintf("%s, max-age=%d", s, int(h.MaxAge.Seconds()))
}

// TTL defines the cache duration.
func TTL(d time.Duration) *time.Duration {
return &d
}

// AddHint applies a caching hint to the request context.
func AddHint(ctx context.Context, hint Hint) {
c := hints(ctx)
if c == nil {
return
}
c <- hint
}

// Hintable extends the context with the ability to add cache hints.
func Hintable(ctx context.Context) (hintCtx context.Context, hint <-chan Hint, done func()) {
hints := make(chan Hint, hintsBuffer)
h := make(chan Hint)
go func() {
h <- resolve(hints)
}()
done = func() {
close(hints)
}
return context.WithValue(ctx, hintsKey, hints), h, done
}

func hints(ctx context.Context) chan Hint {
h, ok := ctx.Value(hintsKey).(chan Hint)
if !ok {
return nil
}
return h
}

func resolve(hints <-chan Hint) Hint {
var minAge *time.Duration
s := ScopePublic
for h := range hints {
if h.Scope == ScopePrivate {
s = h.Scope
}
if h.MaxAge != nil && (minAge == nil || *h.MaxAge < *minAge) {
minAge = h.MaxAge
}
}
if minAge == nil {
var noCache time.Duration
minAge = &noCache
}
return Hint{MaxAge: minAge, Scope: s}
}
43 changes: 43 additions & 0 deletions example/caching/caching.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package caching

import (
"context"
"time"

"github.com/tokopedia/graphql-go/example/caching/cache"
)

const Schema = `
schema {
query: Query
}
type Query {
hello(name: String!): String!
me: UserProfile!
}
type UserProfile {
name: String!
}
`

type Resolver struct{}

func (r Resolver) Hello(ctx context.Context, args struct{ Name string }) string {
cache.AddHint(ctx, cache.Hint{MaxAge: cache.TTL(1 * time.Hour), Scope: cache.ScopePublic})
return "Hello " + args.Name + "!"
}

func (r Resolver) Me(ctx context.Context) *UserProfile {
cache.AddHint(ctx, cache.Hint{MaxAge: cache.TTL(1 * time.Minute), Scope: cache.ScopePrivate})
return &UserProfile{name: "World"}
}

type UserProfile struct {
name string
}

func (p *UserProfile) Name() string {
return p.name
}
139 changes: 139 additions & 0 deletions example/caching/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"

"github.com/tokopedia/graphql-go"
"github.com/tokopedia/graphql-go/example/caching"
"github.com/tokopedia/graphql-go/example/caching/cache"
)

var schema *graphql.Schema

func init() {
schema = graphql.MustParseSchema(caching.Schema, &caching.Resolver{})
}

func main() {
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
}))

http.Handle("/query", &Handler{Schema: schema})

log.Fatal(http.ListenAndServe(":8080", nil))
}

type Handler struct {
Schema *graphql.Schema
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p, ok := h.parseRequest(w, r)
if !ok {
return
}
var response *graphql.Response
var hint *cache.Hint
if cacheable(r) {
ctx, hints, done := cache.Hintable(r.Context())
response = h.Schema.Exec(ctx, p.Query, p.OperationName, p.Variables)
done()
v := <-hints
hint = &v
} else {
response = h.Schema.Exec(r.Context(), p.Query, p.OperationName, p.Variables)
}
responseJSON, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if hint != nil {
w.Header().Set("Cache-Control", hint.String())
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseJSON)
}

func (h *Handler) parseRequest(w http.ResponseWriter, r *http.Request) (params, bool) {
var p params
switch r.Method {
case http.MethodGet:
q := r.URL.Query()
if p.Query = q.Get("query"); p.Query == "" {
http.Error(w, "A non-empty 'query' parameter is required", http.StatusBadRequest)
return params{}, false
}
p.OperationName = q.Get("operationName")
if vars := q.Get("variables"); vars != "" {
if err := json.Unmarshal([]byte(vars), &p.Variables); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return params{}, false
}
}
return p, true
case http.MethodPost:
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return params{}, false
}
return p, true
default:
http.Error(w, fmt.Sprintf("unsupported HTTP method: %s", r.Method), http.StatusMethodNotAllowed)
return params{}, false
}
}

func cacheable(r *http.Request) bool {
return r.Method == http.MethodGet
}

type params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}

var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
const uri = "/query?query=" + encodeURIComponent(graphQLParams.query || "") + "&operationName=" + encodeURIComponent(graphQLParams.operationName || "") + "&variables=" + encodeURIComponent(graphQLParams.variables || "");
return fetch(uri, {
method: "get",
credentials: "include",
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
9 changes: 9 additions & 0 deletions example/social/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Social App

A simple example of how to use struct fields as resolvers instead of methods.

To run this server

`go run ./example/field-resolvers/server/server.go`

and go to localhost:9011 to interact
Loading

0 comments on commit 04acea9

Please sign in to comment.