forked from graph-gophers/graphql-go
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from tokopedia/fork_syncup_with_master_Aug19
Fork syncup with master aug19
- Loading branch information
Showing
39 changed files
with
7,513 additions
and
533 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,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. |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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
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,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} | ||
} |
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,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 | ||
} |
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,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> | ||
`) |
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,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 |
Oops, something went wrong.