Skip to content

Commit ef7ad94

Browse files
committed
jhaadu pocha 🧹
1 parent 725293b commit ef7ad94

File tree

21 files changed

+104
-833
lines changed

21 files changed

+104
-833
lines changed

‎README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,16 @@ Because all GraphQL requests are on a single endpoint and resources are differen
1010

1111
This package takes a GraphQL request, caches the response - if the same request is repeated again, we can serve the response directly from cache without hitting the origin server.
1212

13-
## Why can I just use a key value store to cache?
13+
## Why can't I just use a key value store for caching my GraphQL requests?
1414

1515
You can, but the challenge comes with cache invalidation. How do you identify what fields have changed or have been added so you can invalidate your cache? One way is to invalidate everything for the user - but if you have updates happening often, it becomes as good as no cache.
1616

1717
Since all requests to your GraphQL server pass through `graphql_cache` (including mutations), it can automatically invalidate objects and entire queries for which data has changed. Basically, it supports partial cache invalidation.
1818

1919
## What's the current status of this project?
2020

21-
This currently an experiment, and still a work in progress.
21+
This is still a work in progress, and is not yet ready for production use.
2222

23-
1. Automatic partial cache invalidation doesn't exist
24-
2. Query caching is tested for only the test API and not for all possible query formats/types.
25-
3. Limited to one operation per request.
26-
4. More limitations that I can't even think of now.
23+
## Is there a hosted version?
2724

28-
Once the project is feature complete, it will be open sourced. Once it is ready enough to handle hobby project workloads, it will be open sourced as a complete package.
25+
No. But there will be a Dockerfile you can use to run the Cache server.

‎cache/cache.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cache
22

3+
// Cache is an interface that defines the methods that a cache should implement
4+
// we can have different cache implementations like Redis, Memcached, etc.
35
type Cache interface {
46
Set(key string, value interface{}) error
57
Get(key string) (interface{}, error)

‎cache/cache_response.go

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎cache/inmemory_cache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"graphql_cache/utils/file_utils"
66
)
77

8+
// InMemoryCache implements the Cache interface and uses an in-memory map as the cache store
89
type InMemoryCache struct {
910
cache map[string]interface{}
1011
}

‎cache/redis_cache.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,11 @@ import (
1010

1111
var ctx = context.Background()
1212

13+
// RedisCache implements the Cache interface and uses Redis as the cache store
1314
type RedisCache struct {
1415
cache *redis.Client
1516
}
1617

17-
// var client *redis.Client
18-
19-
// func init() {
20-
// client = redis.NewClient(&redis.Options{
21-
// Addr: "localhost:6379",
22-
// Password: "", // no password set
23-
// DB: 1, // use default DB
24-
// })
25-
// }
26-
2718
func NewRedisCache() Cache {
2819
c := redis.NewClient(&redis.Options{
2920
Addr: "localhost:6379",

‎cache_proxy/balancer/balancer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/labstack/echo/v4/middleware"
66
)
77

8+
// DefaultProxyBalancer is a simple proxy balancer that always proxies to a single target instead of load balancing
89
type DefaultProxyBalancer struct {
910
target *middleware.ProxyTarget
1011
}

‎cache_proxy/cache_middleware/cache_middleware.go

Lines changed: 31 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,57 +4,54 @@ import (
44
"bufio"
55
"bytes"
66
"encoding/json"
7-
"fmt"
87
"graphql_cache/graphcache"
9-
"graphql_cache/transformer"
10-
"graphql_cache/utils/ast_utils"
118
"io"
129
"net"
1310
"net/http"
1411
"time"
1512

13+
"log"
14+
1615
"github.com/labstack/echo/v4"
1716
)
1817

1918
var Cache = graphcache.NewGraphCache("redis")
2019

20+
// CacheMiddleware is a middleware that intercepts the request and response
21+
// and caches the response if it is a query
22+
// if it is a mutation, it does not cache the response, but invalidates the objects which have been mutated
2123
func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
2224
return func(c echo.Context) error {
2325
start := time.Now()
2426

2527
requestBytes, err := io.ReadAll(c.Request().Body)
2628
if err != nil {
27-
fmt.Println("Error reading request body:", err)
29+
log.Println("Error reading request body:", err)
2830
return nil
2931
}
3032

31-
fmt.Println("request content length ", c.Request().ContentLength)
32-
fmt.Println("request body length", len(requestBytes))
33-
3433
var request graphcache.GraphQLRequest
3534
err = json.Unmarshal(requestBytes, &request)
3635
if err != nil {
37-
fmt.Println("Error unmarshalling request:", err)
36+
log.Println("Error unmarshalling request:", err)
3837
return nil
3938
}
4039

41-
astQuery, err := ast_utils.GetASTFromQuery(request.Query)
40+
astQuery, err := graphcache.GetASTFromQuery(request.Query)
4241
if err != nil {
43-
fmt.Println("Error parsing query:", err)
42+
log.Println("Error parsing query:", err)
4443
return nil
4544
}
4645

4746
if astQuery.Operations[0].Operation == "mutation" {
4847
// if the operation is a mutation, we don't cache it
49-
// c.Request().Body = io.NopCloser(bytes.NewBuffer(requestBytes))
50-
// c.Request().ContentLength = -1
51-
transformedBody, err := transformer.TransformBody(request.Query, astQuery)
48+
transformedBody, err := graphcache.AddTypenameToQuery(request.Query)
5249
if err != nil {
53-
fmt.Println("Error transforming body:", err)
50+
log.Println("Error transforming body:", err)
5451
return nil
5552
}
5653

57-
fmt.Println("time taken to transform body ", time.Since(start))
54+
log.Println("time taken to transform body ", time.Since(start))
5855

5956
transformedRequest := request
6057
transformedRequest.Query = transformedBody
@@ -70,58 +67,41 @@ func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
7067
if err != nil {
7168
return err
7269
}
73-
fmt.Println("found a mutation, invalidating cache...")
7470

7571
responseMap := make(map[string]interface{})
7672
err = json.Unmarshal(resBody.Bytes(), &responseMap)
7773
if err != nil {
78-
fmt.Println("Error unmarshalling response:", err)
74+
log.Println("Error unmarshalling response:", err)
7975
}
8076

81-
// go through the response. Every object that has a __typename field, and an id field cache it in the format of typename:id
82-
// for example, if the response has an object with __typename: "Organisation" and id: "1234", cache it as Organisation:1234
83-
// if the object has a nested object with __typename: "User" and id: "5678", cache
84-
// it as User:5678
85-
8677
Cache.InvalidateCache("data", responseMap, nil)
87-
88-
// newResponse := &graphcache.GraphQLResponse{}
89-
// newResponse.FromBytes(resBody.Bytes())
90-
// res, err := Cache.RemoveTypenameFromResponse(newResponse)
91-
// if err != nil {
92-
// fmt.Println("Error removing __typename:", err)
93-
// return nil
94-
// }
95-
// fmt.Println("response bytes ", string(res.Bytes()))
96-
// c.Response().Write(res.Bytes())
97-
// c.Response().Header().Set("X-Proxy", "GraphQL Cache")
9878
return nil
9979
}
10080

10181
cachedResponse, err := Cache.ParseASTBuildResponse(astQuery, request)
10282
if err == nil && cachedResponse != nil {
103-
fmt.Println("serving response from cache...")
83+
log.Println("serving response from cache...")
10484
br, err := json.Marshal(cachedResponse)
10585
if err != nil {
10686
return err
10787
}
108-
fmt.Println("time taken to serve response from cache ", time.Since(start))
88+
log.Println("time taken to serve response from cache ", time.Since(start))
10989
graphqlresponse := graphcache.GraphQLResponse{Data: json.RawMessage(br)}
11090
res, err := Cache.RemoveTypenameFromResponse(&graphqlresponse)
11191
if err != nil {
112-
fmt.Println("Error removing __typename:", err)
92+
log.Println("Error removing __typename:", err)
11393
return nil
11494
}
11595
return c.JSON(200, res)
11696
}
11797

118-
transformedBody, err := transformer.TransformBody(request.Query, astQuery)
98+
transformedBody, err := graphcache.AddTypenameToQuery(request.Query)
11999
if err != nil {
120-
fmt.Println("Error transforming body:", err)
100+
log.Println("Error transforming body:", err)
121101
return nil
122102
}
123103

124-
fmt.Println("time taken to transform body ", time.Since(start))
104+
log.Println("time taken to transform body ", time.Since(start))
125105

126106
transformedRequest := request
127107
transformedRequest.Query = transformedBody
@@ -140,18 +120,18 @@ func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
140120
responseMap := make(map[string]interface{})
141121
err = json.Unmarshal(resBody.Bytes(), &responseMap)
142122
if err != nil {
143-
fmt.Println("Error unmarshalling response:", err)
123+
log.Println("Error unmarshalling response:", err)
144124
}
145125

146-
fmt.Println("time taken to get response from API ", time.Since(start))
126+
log.Println("time taken to get response from API ", time.Since(start))
147127

148-
astWithTypes, err := ast_utils.GetASTFromQuery(transformedRequest.Query)
128+
astWithTypes, err := graphcache.GetASTFromQuery(transformedRequest.Query)
149129
if err != nil {
150-
fmt.Println("Error parsing query:", err)
130+
log.Println("Error parsing query:", err)
151131
return nil
152132
}
153133

154-
fmt.Println("time taken to generate AST with types ", time.Since(start))
134+
log.Println("time taken to generate AST with types ", time.Since(start))
155135

156136
reqVariables := transformedRequest.Variables
157137
variables := make(map[string]interface{})
@@ -160,9 +140,7 @@ func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
160140
}
161141

162142
for _, op := range astWithTypes.Operations {
163-
// for the operation op we need to traverse the response and the ast together to build a graph of the relations
164-
165-
// build the relation graph
143+
// for the operation op we need to traverse the response and build the relationship map where key is the requested field and value is the key where the actual response is stored in the cache
166144
responseKey := Cache.GetQueryResponseKey(op, responseMap, variables)
167145
for key, value := range responseKey {
168146
if value != nil {
@@ -171,7 +149,7 @@ func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
171149
}
172150
}
173151

174-
fmt.Println("time taken to build response key ", time.Since(start))
152+
log.Println("time taken to build response key ", time.Since(start))
175153

176154
// go through the response. Every object that has a __typename field, and an id field cache it in the format of typename:id
177155
// for example, if the response has an object with __typename: "Organisation" and id: "1234", cache it as Organisation:1234
@@ -180,22 +158,9 @@ func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
180158

181159
Cache.CacheResponse("data", responseMap, nil)
182160

183-
fmt.Println("time taken to cache response ", time.Since(start))
184-
185-
// cacheStore.Debug("cacheStore")
186-
// recordCacheStore.Debug("recordCacheStore")
187-
// queryCacheStore.Debug("queryCacheStore")
188-
189-
// cacheState, _ := cacheStore.JSON()
190-
// fmt.Println(string(cacheState))
191-
192-
// recordCacheState, _ := recordCacheStore.JSON()
193-
// fmt.Println(string(recordCacheState))
194-
195-
// queryCacheState, _ := queryCacheStore.JSON()
196-
// fmt.Println(string(queryCacheState))
161+
log.Println("time taken to cache response ", time.Since(start))
197162

198-
fmt.Println("time taken to finish completely ", time.Since(start))
163+
// remove __typename from the response
199164
newResponse := &graphcache.GraphQLResponse{}
200165
newResponse.FromBytes(resBody.Bytes())
201166
Cache.RemoveTypenameFromResponse(newResponse)
@@ -204,6 +169,9 @@ func CacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
204169
}
205170
}
206171

172+
// bodyDumpResponseWrite is a custom response writer that writes the response body to a buffer
173+
// and also writes it to the actual response writer
174+
// this is used to read the response body after it has been written to the response writer
207175
type bodyDumpResponseWriter struct {
208176
io.Writer
209177
http.ResponseWriter

‎cache_proxy/main.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ func main() {
1818
e.Use(middleware.Recover())
1919
e.Use(middleware.Logger())
2020

21-
// e.Use(cache_middleware.NewCacheMiddleware(gc))
2221
apiSever, err := url.Parse(API_URL)
2322
if err != nil {
2423
e.Logger.Fatal(err)
@@ -53,25 +52,6 @@ func main() {
5352

5453
g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{
5554
Balancer: balancer,
56-
ModifyResponse: func(resp *http.Response) error {
57-
// responseBody, err := io.ReadAll(resp.Body)
58-
// if err != nil {
59-
// fmt.Println("Error reading response body:", err)
60-
// return err
61-
// }
62-
// newResponse := &graphcache.GraphQLResponse{}
63-
// newResponse.FromBytes(responseBody)
64-
// res, err := cache_middleware.Cache.RemoveTypenameFromResponse(newResponse)
65-
// if err != nil {
66-
// fmt.Println("Error removing __typename:", err)
67-
// return nil
68-
// }
69-
// body := io.NopCloser(bytes.NewReader(res.Bytes()))
70-
// resp.Body = body
71-
// resp.ContentLength = int64(len(res.Bytes()))
72-
// resp.Header.Set("Content-Length", strconv.Itoa(len(res.Bytes())))
73-
return nil
74-
},
7555
}))
7656

7757
e.Logger.Fatal(e.Start(":9090"))

‎utils/ast_utils/ast_utils.go renamed to ‎graphcache/ast.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package ast_utils
1+
package graphcache
22

33
import (
44
"github.com/vektah/gqlparser/ast"

‎graphcache/graphcache.go

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,13 @@ import (
1212
"github.com/vektah/gqlparser/ast"
1313
)
1414

15+
// GraphCache is a struct that holds the cache stores for the GraphQL cache
1516
type GraphCache struct {
1617
cacheStore cache.Cache
1718
recordCacheStore cache.Cache
1819
queryCacheStore cache.Cache
1920
}
2021

21-
type GraphQLRequest struct {
22-
Query string `json:"query"`
23-
Variables map[string]interface{} `json:"variables"`
24-
}
25-
26-
func (gr *GraphQLRequest) Map() map[string]interface{} {
27-
return map[string]interface{}{
28-
"query": gr.Query,
29-
"variables": gr.Variables,
30-
}
31-
}
32-
33-
func (gr *GraphQLRequest) Bytes() []byte {
34-
bytes, _ := json.Marshal(gr)
35-
return bytes
36-
}
37-
38-
type GraphQLResponse struct {
39-
Data json.RawMessage `json:"data"`
40-
Errors []interface{} `json:"errors"`
41-
}
42-
43-
func (gr *GraphQLResponse) Map() map[string]interface{} {
44-
return map[string]interface{}{
45-
"data": gr.Data,
46-
"errors": gr.Errors,
47-
}
48-
}
49-
50-
func (gr *GraphQLResponse) Bytes() []byte {
51-
bytes, _ := json.Marshal(gr)
52-
return bytes
53-
}
54-
55-
func (gr *GraphQLResponse) FromBytes(bytes []byte) {
56-
json.Unmarshal(bytes, gr)
57-
}
58-
5922
func NewGraphCache(backend string) *GraphCache {
6023
if backend == "redis" {
6124
return &GraphCache{
@@ -399,8 +362,6 @@ func (gc *GraphCache) CacheResponse(field string, object map[string]interface{},
399362
cacheKey := gc.CacheObject(field, object, parent)
400363

401364
return object, cacheKey
402-
403-
// return parent
404365
}
405366

406367
func (gc *GraphCache) GetQueryResponseKey(queryDoc *ast.OperationDefinition, response map[string]interface{}, variables map[string]interface{}) map[string]interface{} {

0 commit comments

Comments
 (0)