defmodule GitHub.GraphQL do
@doc """
Query the GitHub GraphQL API
Nothing fancy here like `GitHub.GraphQL.paginate`: the query is simply
submitted and the response lightly parsed.
The happy path is a response with a 200 status code and a body that contains
a "data" field. If this is the case then `{:ok, data}` is returned.
Otherwise various well known error conditions are recognized and will be
returned as `{:error, some_error_value}`
"""
def query(query, config, vars \\ %{first: 20}) do
query
|> call_graphql(config, vars)
|> case do
{:ok, %Neuron.Response{status_code: 200, body: %{"errors" => errors}}} ->
{:error, errors}
{:ok, %Neuron.Response{status_code: 200, body: %{"data" => data}}} ->
{:ok, data}
{:error, %Neuron.Response{body: body}} ->
{:error, body}
error ->
{:error, error}
end
end
@doc """
Make a paginated query against the GitHub GraphQL API
This requires that the query itself support pagination.
1. The query must define `$first: Int` and `$after: String` variables
2. The query must use the `$first` and `$after` variables
3. The query must request `pageInfo { hasNextPage, endCursor }`
## Example query that supports automatic pagination
```graphql
query($first: Int, $after: String) {
viewer {
followers(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
nodes {
databaseId
}
}
}
}
```
The `nodes` data will be collected into a list and returned.
"""
def paginate(query, config, vars \\ %{first: 20}) do
init = fn -> request(query, config, vars) end
next = &next/1
stop = &Function.identity/1
Stream.resource(init, next, stop)
|> Enum.into([])
|> case do
[] ->
IO.puts("Empty results from pagination. Did your query have a `nodes` field?")
pagination_usage()
[]
nodes ->
nodes
end
end
defp request(query, config, vars) do
query
|> call_graphql(config, vars)
|> case do
{:ok, %Neuron.Response{status_code: 200, body: %{"errors" => errors}}} ->
IO.inspect(errors)
{[], nil, query, config, vars}
{:ok, %Neuron.Response{status_code: 200, body: %{"data" => data}}} ->
{GraphQL.find_nodes(data), GraphQL.find_pageinfo(data), query, config, vars}
{:error, %Neuron.Response{body: body}} ->
IO.inspect(body)
{[], nil, query, config, vars}
error ->
IO.inspect(error)
{[], nil, query, config, vars}
end
end
defp next({nil, _query, _config, _vars} = acc), do: {:halt, acc}
defp next({%{"hasNextPage" => false}, _query, _config, _vars} = acc), do: {:halt, acc}
defp next({%{"endCursor" => endCursor}, query, config, vars} = acc) do
results = request(query, config, Map.put(vars, :after, endCursor))
case results do
{_data, %{"endCursor" => ^endCursor}, _query, _config, _vars} ->
IO.puts("endCursor is not advancing: halting queries")
pagination_usage()
{:halt, acc}
_ ->
next(results)
end
end
defp next({data, page, query, config, args}) do
{data, {page, query, config, args}}
end
defp call_graphql(query, config, vars) do
Neuron.query(query, vars,
url: config[:endpoint],
headers: [authorization: "Bearer #{config[:token]}"]
)
end
defp pagination_usage() do
IO.puts("For pagination to work your query must meet the requirements")
IO.puts("""
query($first: Int, $after: String) {
viewer {
followers(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
nodes {
databaseId
}
}
}
}
""")
end
end