Skip to content

Commit 98f0c7b

Browse files
committed
Doco and reorg
1 parent 2dd53b4 commit 98f0c7b

7 files changed

Lines changed: 145 additions & 95 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
# Graphql Examples
3+
4+
This splodge of code is mostly my personal explorations of `graphql-java` and how to use it in more real world examples.
5+
6+
I thought I would try and make it in "example" format so that others might derive some value from it.
7+
8+
I also plan to make some blog posts about this in the near future and use this code as the basis for that.
9+
10+
Its built with Gradle and Java, with some Spock Groovy unit testing.
11+
12+
## Example 1 - HTTP Proxy of an existing REST API
13+
14+
The first example is the idea of fronting an existing REST API with graphql.
15+
16+
In this case *An API of Ice And Fire*
17+
18+
https://anapioficeandfire.com/Documentation
19+
20+
This is not an original idea, others have done it in Node.js but hey I am a Java guy.
21+
22+
The code is in `com.graphql.example.proxy.IceAndFireApiProxy`
23+
24+
I wanted to explore the following:
25+
26+
- Hosting a simple HTTP server of graphql - using Jetty
27+
- A Relay shape that turns old school page=N pagination into cursors eg https://facebook.github.io/relay/docs/getting-started.html
28+
- Using the `java-dataloader` to make an efficient REST proxy eg: https://github.com/graphql-java/java-dataloader
29+
30+
What I learnt was :
31+
32+
- `CompleteableFuture` support in graphql-java is da bomb! Wicked good.
33+
- Used well, `java-dataloader` is da double bomb! By priming the cache and using URLs as keys we cache the sheet out of everything
34+
- Relay pagination is tricky. Not impossible but tricky with forward only pagination.
35+
- The Relay support in graphql-java is basic (it doesn't pretend to be more I guess) and really on works if you have a complete list
36+
of edges in memory
37+
38+
39+
40+

src/main/java/com/graphql/example/proxy/IceAndFireDataFetchers.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.graphql.example.proxy;
22

3+
import com.graphql.example.proxy.relay.ForwardOnlyFixedPagedDataSet;
4+
import com.graphql.example.proxy.relay.PagedResult;
35
import com.graphql.example.util.HttpClient;
46
import com.graphql.example.util.RelayUtils;
57
import graphql.relay.Connection;
@@ -150,9 +152,9 @@ DataFetcher characters() {
150152
pageNumber -> readPagedObjects("characters", pageNumber)));
151153
}
152154

153-
private ForwardOnlyFixedPagedDataSet.PagedResult<Map<String, Object>> readPagedObjects(String resource, int pageNumber) {
155+
private PagedResult<Map<String, Object>> readPagedObjects(String resource, int pageNumber) {
154156
log.info("Fetching {} page: {}", resource, pageNumber);
155-
ForwardOnlyFixedPagedDataSet.PagedResult<Map<String, Object>> pagedResult =
157+
PagedResult<Map<String, Object>> pagedResult =
156158
HttpClient.readResource(resource, qp("pageNumber", pageNumber), qp("pageSize", PAGE_SIZE));
157159

158160
log.info("\tread {} {}", pagedResult.getResults().size(), resource);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.graphql.example.proxy.relay;
2+
3+
import graphql.relay.ConnectionCursor;
4+
import graphql.relay.DefaultConnectionCursor;
5+
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.regex.Matcher;
8+
import java.util.regex.Pattern;
9+
10+
/**
11+
* This uses an encoding of page # plus full offset from the page forward
12+
*/
13+
class CursorPageAndOffset {
14+
private static final java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
15+
private static final java.util.Base64.Decoder decoder = java.util.Base64.getDecoder();
16+
private final static Pattern pagePattern = Pattern.compile("^page=([0-9]*)");
17+
private final static Pattern offsetPattern = Pattern.compile(".*;offset=([0-9]*)");
18+
19+
int page;
20+
int offset;
21+
22+
CursorPageAndOffset(int page, int offset) {
23+
this.page = page;
24+
this.offset = offset;
25+
}
26+
27+
int getPage() {
28+
return page;
29+
}
30+
31+
int getOffset() {
32+
return offset;
33+
}
34+
35+
public static CursorPageAndOffset fromCursor(String cursor) {
36+
String s = decode(cursor);
37+
Matcher matcher = pagePattern.matcher(s);
38+
if (!matcher.find()) {
39+
throwInvalidCursor(s);
40+
}
41+
String page = matcher.group(1);
42+
43+
matcher = offsetPattern.matcher(s);
44+
if (!matcher.find()) {
45+
throwInvalidCursor(s);
46+
}
47+
String offset = matcher.group(1);
48+
return new CursorPageAndOffset(Integer.parseInt(page), Integer.parseInt(offset));
49+
}
50+
51+
ConnectionCursor toConnectionCursor() {
52+
return new DefaultConnectionCursor(encode("page=" + page + ";offset=" + offset));
53+
}
54+
55+
private String encode(String s) {
56+
return encoder.encodeToString(s.getBytes(StandardCharsets.UTF_8));
57+
}
58+
59+
static private String decode(String s) {
60+
return new String(decoder.decode(s), StandardCharsets.UTF_8);
61+
}
62+
63+
private static void throwInvalidCursor(String cursor) {
64+
throw new IllegalArgumentException("Invalid paged cursor provided : " + cursor);
65+
}
66+
}

src/main/java/com/graphql/example/proxy/ForwardOnlyFixedPagedDataSet.java renamed to src/main/java/com/graphql/example/proxy/relay/ForwardOnlyFixedPagedDataSet.java

Lines changed: 4 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
package com.graphql.example.proxy;
1+
package com.graphql.example.proxy.relay;
22

33
import graphql.relay.Connection;
44
import graphql.relay.ConnectionCursor;
55
import graphql.relay.DefaultConnection;
6-
import graphql.relay.DefaultConnectionCursor;
76
import graphql.relay.DefaultEdge;
87
import graphql.relay.DefaultPageInfo;
98
import graphql.relay.Edge;
109
import graphql.relay.PageInfo;
1110
import graphql.schema.DataFetchingEnvironment;
1211

13-
import java.nio.charset.StandardCharsets;
1412
import java.util.ArrayList;
1513
import java.util.Collections;
1614
import java.util.List;
1715
import java.util.function.Function;
18-
import java.util.regex.Matcher;
19-
import java.util.regex.Pattern;
2016

2117
//
2218
// the ice and fire API uses page=n&pageSize=n pagination and we cant know the
@@ -31,87 +27,6 @@
3127
public class ForwardOnlyFixedPagedDataSet {
3228

3329

34-
/**
35-
* This uses an encoding of page # plus full offset from the page forward
36-
*/
37-
static class PageAndOffset {
38-
private static final java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
39-
private static final java.util.Base64.Decoder decoder = java.util.Base64.getDecoder();
40-
private final static Pattern pagePattern = Pattern.compile("^page=([0-9]*)");
41-
private final static Pattern offsetPattern = Pattern.compile(".*;offset=([0-9]*)");
42-
43-
int page;
44-
int offset;
45-
46-
PageAndOffset(int page, int offset) {
47-
this.page = page;
48-
this.offset = offset;
49-
}
50-
51-
int getPage() {
52-
return page;
53-
}
54-
55-
int getOffset() {
56-
return offset;
57-
}
58-
59-
public static PageAndOffset fromCursor(String cursor) {
60-
String s = decode(cursor);
61-
Matcher matcher = pagePattern.matcher(s);
62-
if (!matcher.find()) {
63-
throwInvalidCursor(s);
64-
}
65-
String page = matcher.group(1);
66-
67-
matcher = offsetPattern.matcher(s);
68-
if (!matcher.find()) {
69-
throwInvalidCursor(s);
70-
}
71-
String offset = matcher.group(1);
72-
return new PageAndOffset(Integer.parseInt(page), Integer.parseInt(offset));
73-
}
74-
75-
ConnectionCursor toConnectionCursor() {
76-
return new DefaultConnectionCursor(encode("page=" + page + ";offset=" + offset));
77-
}
78-
79-
private String encode(String s) {
80-
return encoder.encodeToString(s.getBytes(StandardCharsets.UTF_8));
81-
}
82-
83-
static private String decode(String s) {
84-
return new String(decoder.decode(s), StandardCharsets.UTF_8);
85-
}
86-
87-
private static void throwInvalidCursor(String cursor) {
88-
throw new IllegalArgumentException("Invalid paged cursor provided : " + cursor);
89-
}
90-
}
91-
92-
/**
93-
* The results that come back from the page retrieval function need to tell us
94-
* the list of results and the whether their is a next page or not
95-
*/
96-
public static class PagedResult<T> {
97-
private final List<T> results;
98-
private final boolean hasNextPage;
99-
100-
public PagedResult(List<T> results, boolean hasNextPage) {
101-
this.results = results;
102-
this.hasNextPage = hasNextPage;
103-
}
104-
105-
public List<T> getResults() {
106-
return results;
107-
}
108-
109-
public boolean hasNextPage() {
110-
return hasNextPage;
111-
}
112-
}
113-
114-
11530
/**
11631
* Called to get a realy {@link graphql.relay.Connection} of edges where the underlying dataset
11732
* is a set of fixed size pages of data that can ONLY be read in a forward only manner
@@ -129,10 +44,10 @@ public static <T> Connection<T> getConnection(DataFetchingEnvironment env, int d
12944
throw new IllegalArgumentException("You must provide a positive value for 'first'");
13045
}
13146
boolean afterPresent = env.getArgument("after") != null;
132-
String zeroZeroDefault = new PageAndOffset(0, 0).toConnectionCursor().toString();
47+
String zeroZeroDefault = new CursorPageAndOffset(0, 0).toConnectionCursor().toString();
13348
String afterCursor = getArg(env, "after", zeroZeroDefault);
13449

135-
PageAndOffset desiredPageAndOffset = PageAndOffset.fromCursor(afterCursor);
50+
CursorPageAndOffset desiredPageAndOffset = CursorPageAndOffset.fromCursor(afterCursor);
13651
int startPage = desiredPageAndOffset.getPage();
13752

13853
List<Edge<T>> edges = new ArrayList<>();
@@ -144,7 +59,7 @@ public static <T> Connection<T> getConnection(DataFetchingEnvironment env, int d
14459

14560
PagedResult<T> pagedResult = pageOfDataRetriever.apply(startPage);
14661
for (T obj : pagedResult.getResults()) {
147-
ConnectionCursor edgeCursor = new PageAndOffset(startPage, fullOffset).toConnectionCursor();
62+
ConnectionCursor edgeCursor = new CursorPageAndOffset(startPage, fullOffset).toConnectionCursor();
14863
if (fullOffset == desiredPageAndOffset.getOffset()) {
14964
addToEdges = true;
15065
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.graphql.example.proxy.relay;
2+
3+
import java.util.List;
4+
5+
/**
6+
* The results that come back from the page retrieval function need to tell us
7+
* the list of results and the whether their is a next page or not
8+
*/
9+
public class PagedResult<T> {
10+
private final List<T> results;
11+
private final boolean hasNextPage;
12+
13+
public PagedResult(List<T> results, boolean hasNextPage) {
14+
this.results = results;
15+
this.hasNextPage = hasNextPage;
16+
}
17+
18+
public List<T> getResults() {
19+
return results;
20+
}
21+
22+
public boolean hasNextPage() {
23+
return hasNextPage;
24+
}
25+
}

src/main/java/com/graphql/example/util/HttpClient.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.graphql.example.util;
22

3-
import com.graphql.example.proxy.ForwardOnlyFixedPagedDataSet;
3+
import com.graphql.example.proxy.relay.PagedResult;
44
import okhttp3.HttpUrl;
55
import okhttp3.OkHttpClient;
66
import okhttp3.Request;
@@ -38,7 +38,7 @@ public Object getData() {
3838
}
3939
}
4040

41-
public static <T> ForwardOnlyFixedPagedDataSet.PagedResult<T> readResource(String resource, HttpQueryParameter... params) {
41+
public static <T> PagedResult<T> readResource(String resource, HttpQueryParameter... params) {
4242
HttpUrl.Builder urlBuilder = new HttpUrl.Builder();
4343
urlBuilder.scheme("https").host("www.anapioficeandfire.com").addPathSegment("api").addPathSegment(resource);
4444
if (params != null) {
@@ -51,7 +51,7 @@ public static <T> ForwardOnlyFixedPagedDataSet.PagedResult<T> readResource(Strin
5151
DataAndResponse dataAndResponse = readResourceUrl(url);
5252
//noinspection unchecked
5353
List<T> data = (List<T>) dataAndResponse.getData();
54-
return new ForwardOnlyFixedPagedDataSet.PagedResult<>(data, hasNext(dataAndResponse.getResponse()));
54+
return new PagedResult<>(data, hasNext(dataAndResponse.getResponse()));
5555
}
5656

5757
//

src/test/groovy/com/graphql/example/proxy/ForwardOnlyFixedPagedDataSetTest.groovy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.graphql.example.proxy
22

3+
import com.graphql.example.proxy.relay.ForwardOnlyFixedPagedDataSet
4+
import com.graphql.example.proxy.relay.PagedResult
35
import graphql.schema.DataFetchingEnvironment
46
import graphql.schema.DataFetchingEnvironmentBuilder
57
import spock.lang.Specification
@@ -82,7 +84,7 @@ class ForwardOnlyFixedPagedDataSetTest extends Specification {
8284
for (int i = 0; i < count; i++) {
8385
l.add("item" + i)
8486
}
85-
return new ForwardOnlyFixedPagedDataSet.PagedResult(l, hasNextPage)
87+
return new PagedResult(l, hasNextPage)
8688
}
8789

8890
}

0 commit comments

Comments
 (0)