Pagination¶
For performance reasons, queries that return multiple statements return only the first 100 statements matched by the query selection criteria. If you wish to retrieve more than 100 statements, you must page through the statements using the techniques described below. The OtterTax API uses a modified version of the connection specification defined by the Relay projecct.
Pagination is defined on all queries which return statements, both the generic statement object and the various distinct statement objects. These queries are:
- getStatements
- getF1098Statements
- getF1098eStatements
- getF1098tStatements
- getF1099divStatements
- getF1099intStatements
- getF1099miscStatements
- getF1099necStatements
- getF1099rStatements
- getF3921Statements
- getF3922Statements
- getF5498Statements
- getFw2Statements
The following discussion uses the getStatements
query to explain pagination, but the concepts apply to all of the queries in the preceding list. The discussion assumes that your query does not include any parameters and that you are therefore retrieving all statements. See the documentation for reviewing all statement types for information about limiting the scope of your query.
Query Outline¶
The format of the getStatements
query is shown below. For the sake of readability, only the OtterTax ID of each statement is returned, but you can, obviously, return any other fields available on the statement.
query {
getStatements {
errors
statements {
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
nodes {
otxId
}
edges {
cursor
node {
otxId
}
}
}
}
}
errors
object (which is returned by all OtterTax queries and mutations) along with a statements
object.
The statements
Object¶
The statements
object is a connection object which accepts pagination parameters and returns a matching list of statements as both nodes and edges.
Statement Parameters¶
The statements
object accepts four parameters: after, before, first, and last. Using a combination of these parameters allows you to page forward or backward through the list of matching statements.
Statement Return Values¶
The statements
object returns the following three fields:
- A pageInfo object which contains information about paging through the dataset.
- A list of nodes, which, in this query, are statements.
- A list of edges. Each edge contains a node along with a cursor for that node.
The PageInfo Object¶
The pageInfo object contains metadata required to page through the statements. It has four elements:
endCursor
The cursor of the last statement returned in the list. When paging forward, use the value in this field as theafter
parameter in the next call.hasNextPage
Used when paging forward. True if there are additional statements to be retrieved, false otherwise.hasPreviousPage
Used when paging backward. True if there are additional statements to be retrieved, false otherwise.startCursor
The cursor of the first statement returned in the list. When paging backward, use the value in this field as thebefore
parameter in the next call.
The Nodes List¶
The list of nodes returned by a call to getStatements
is an array of statement objects. If you don't need cursor information for statements, use the nodes list to read statements.
The Edges List¶
The list of edges includes all of the information in the nodes list. It also includes a cursor value for every statement returned.
Pagination Methodology¶
To page through the list of statements, use the data returned in the pageInfo
object to scroll through the dataset, either backward or forward.
Forward Pagination¶
To page forward through the statements, supply provide values for the first
and after
parameters. The value for first
should be a non-negative integer indicating the number of statementst to return, and the value of after
should be a string identifying the cursor of the statement after which the query should start returning statements. The statement denoted by the cursor will not be returned by the query. Use the pageInfo
object or the cursor returned in the edges list to determine a value to supply to the after
parameter.
Example
Supplying values of 75 and "MQ7" retrieves 75 statements after the statement with an associated cursor of "MQ7."
Backward Pagination¶
To page backward through the statements, supply provide values for the last
and before
parameters. The value for last
should be a non-negative integer indicating the number of statementst to return, and the value of before
should be a string identifying the cursor of the statement before which the query should start returning statements. The statement denoted by the cursor will not be returned by the query. Use the pageInfo
object or the cursor returned in the edges list to determine a value to supply to the before
parameter.
Example
Supplying values of 90 and "zR68" retrieves 90 statements before the statement with an associated cursor of "zR68."
Code Examples: Paging Through Records¶
The pseudocode below illustrates paging forward through a list of statements. It retrieves records in groups of 50, but the API supports batch sizes of up to 100.
set moreStatements = true
set endCursor = null
until moreStatements = false
get 50 records
get endCursor
set moreStatements = hasNextPage
end
The code snippets below demonstrate various implementations of this pseudocode. See the documentation on authentication for information about obtaining the credential data passed in the header.
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
// This example uses version 2.9.1 of the
// open source gson library from Google.
// See https://github.com/google/gson.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
// This example uses httpclient (version 4.5.13)
// and httpcore (version 4.4.13) libraries of the
// open source Apache HttpComponents project.
// See https://hc.apache.org/index.html.
import org.apache.http.entity.StringEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
// Download the OtterTax java classes for the GraphQL
// query and mutation responses at
// https://github.com/OtterTax/graphql-java-classes
import com.ottertax.support.Statement;
import com.ottertax.support.GetStatementsResponse;
class Paginator {
private String endpoint = "https://sandbox.ottertax.com/v2/graphql";
private String accessToken = "YOUR ACCESS TOKEN";
private String client = "YOUR CLIENT ID";
private String uid = "YOUR UID";
private String buildGQL(String endCursor) {
StringBuilder sb = new StringBuilder();
sb.append("query {\n" +
" getStatements {\n" +
" errors\n" +
" statements( first: 50");
if(endCursor != null) {
sb.append( " after: \"" + endCursor + "\"" );
}
sb.append(" ) {\n" +
" pageInfo {\n" +
" hasNextPage\n" +
" endCursor\n" +
" hasPreviousPage\n" +
" startCursor\n" +
" }\n" +
" nodes {\n" +
" otxId\n" +
" }\n" +
" }\n" +
" }\n" +
"}\n");
return(sb.toString());
}
private String querify(String rawGraphql) {
Gson gson = new Gson();
return("{\"query\":" + gson.toJson(rawGraphql) + "}");
}
private String getBatch(String endCursor) {
CloseableHttpClient httpClient = HttpClients.createDefault();
String response = "";
try {
HttpPost httpPost = new HttpPost(endpoint);
httpPost.addHeader("content-type", "application/json");
httpPost.addHeader("access-token", accessToken);
httpPost.addHeader("client", client);
httpPost.addHeader("uid", uid);
String gql = buildGQL(endCursor);
StringEntity stringEntity = new StringEntity(querify(gql));
httpPost.setEntity(stringEntity);
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
BufferedReader reader = new BufferedReader(new InputStreamReader(
httpResponse.getEntity().getContent()));
StringBuffer responseBuffer = new StringBuffer();
String inputLine;
while ((inputLine = reader.readLine()) != null) {
responseBuffer.append(inputLine);
}
reader.close();
response = responseBuffer.toString();
int responseCode = httpResponse.getStatusLine().getStatusCode();
if( responseCode != 200 ) {
System.out.println("Response code from server was " + String.valueOf(responseCode) + ".");
}
httpResponse.close();
httpClient.close();
} catch(IOException e) {
System.out.println("Error posting GraphQL.\nExiting");
System.exit(1);
}
return(response);
}
private void get() {
int statementCount = 0;
boolean moreStatements = true;
String endCursor = null;
Gson gson = new GsonBuilder().setPrettyPrinting().create();
while(moreStatements == true) {
String response = getBatch(endCursor);
GetStatementsResponse getStatementsResponse = gson.fromJson(response, GetStatementsResponse.class);
Statement[] statements = getStatementsResponse.getData().getGetStatements().getStatements().getNodes();
statementCount += statements.length;
endCursor = getStatementsResponse.getData().getGetStatements().getStatements().getPageInfo().getEndCursor();
moreStatements = getStatementsResponse.getData().getGetStatements().getStatements().getPageInfo().getHasNextPage();
}
System.out.println("Retrieved " + String.valueOf(statementCount) + " statements.");
}
public static void main(String[] args) {
Paginator paginator = new Paginator();
paginator.get();
}
}
// Using graphql-request from
// https://github.com/prisma-labs/graphql-request
// Example tested with node version 14.16.0
import { GraphQLClient, gql } from 'graphql-request'
async function main() {
const endpoint = 'https://sandbox.ottertax.com/v2/graphql'
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
'access-token': 'YOUR ACCESS TOKEN',
'client': 'YOUR CLIENT ID',
'uid': 'YOUR UID'
},
})
let statementCount = 0
let moreStatements = true
let endCursor = null
while(moreStatements) {
let parameters = ['first: 50']
if(endCursor != null) {
parameters.push( `after: "${endCursor}"` )
}
const query = gql`
query {
getStatements {
errors
statements( ${parameters.join( ' ' )} ) {
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
nodes {
otxId
}
}
}
}
`
let data = await graphQLClient.request(query)
let statements = data.getStatements.statements.nodes
moreStatements = data.getStatements.statements.pageInfo.hasNextPage
endCursor = data.getStatements.statements.pageInfo.endCursor
statementCount += statements.length
}
console.log(`Retrieved ${statementCount} statements.`)
}
main().catch((error) => console.error(error))
<?php
// Tested with php-cli version 8.0.5.
$statement_count = 0;
$more_statements = TRUE;
$end_cursor = NULL;
while($more_statements == TRUE) {
$parameters = array("first: 50");
if($end_cursor != NULL) {
array_push($parameters, "after: \"{$end_cursor}\"");
}
$argument_list = join( ' ', $parameters );
$query =<<<"END_DATA"
query {
getStatements {
errors
statements( {$argument_list} ) {
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
nodes {
otxId
}
}
}
}
END_DATA;
$payload = array ('query' => $query);
$options = array(
'http' => array(
'method' => 'POST',
'content' => json_encode( $payload ),
'header'=> "Content-Type: application/json\r\n" .
"access-token: YOUR ACCESS TOKEN\r\n" .
"client: YOUR CLIENT ID\r\n" .
"uid: YOUR UID\r\n"
)
);
$context = stream_context_create( $options );
$response = file_get_contents( 'https://sandbox.ottertax.com/v2/graphql',
false, $context );
if( $response === FALSE ) {
echo "Call to server failed.\n";
} else {
$json = json_decode( $response, true );
$statements = $json['data']['getStatements']['statements']['nodes'];
$more_statements = $json['data']['getStatements']['statements']['pageInfo']['hasNextPage'];
$end_cursor = $json['data']['getStatements']['statements']['pageInfo']['endCursor'];
$statement_count += count($statements);
}
}
echo "Retrieved {$statement_count} statements.\n";
?>
# Using GQL from
# https://github.com/graphql-python/gql
# Tested using python version 3.8.8
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(url="https://sandbox.ottertax.com/v2/graphql",
headers={ 'access-token': 'YOUR ACCESS TOKEN',
'client': 'YOUR CLIENT ID',
'uid': 'YOUR UID' })
client = Client(transport=transport, fetch_schema_from_transport=True)
statement_count = 0
more_statements = True
end_cursor = None
while more_statements == True:
parameters = ['first: 50']
if not end_cursor == None:
parameters.append(f'after: "{end_cursor}"')
query = gql(
"""
query {
getStatements {
errors
statements( %s ) {
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
nodes {
otxId
}
}
}
}
""" % (" ".join(parameters))
)
result = client.execute(query)
statements = result['getStatements']['statements']['nodes']
more_statements = result['getStatements']['statements']['pageInfo']['hasNextPage']
end_cursor = result['getStatements']['statements']['pageInfo']['endCursor']
statement_count += len(statements)
print(f'Retrieved {statement_count} statements.')
# If you wish to use a library instead, see
# https://github.com/github/graphql-client
# Tested using ruby 2.7.2.
require( 'net/http' )
require( 'uri' )
require( 'json' )
uri = URI( "https://sandbox.ottertax.com/v2/graphql" )
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
headers = { 'Content-Type': 'application/json',
'access-token': 'YOUR ACCESS TOKEN',
'client': 'YOUR CLIENT ID',
'uid': 'YOUR UID' }
statement_count = 0
more_statements = true
end_cursor = nil
while( more_statements ) do
parameters = ['first: 50']
if( end_cursor )
parameters << "after: \"#{end_cursor}\""
end
query = <<-END_DATA
query {
getStatements {
errors
statements( #{parameters.join( ' ' )} ) {
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
nodes {
otxId
}
}
}
}
END_DATA
request = Net::HTTP::Post.new(uri.request_uri, headers )
request.body = {query: query}.to_json
response = http.request(request)
if( response.code == '200' )
payload = JSON.parse( response.body )
errors = payload.dig( 'data', 'getStatements', 'errors' )
if( errors.any? )
STDOUT.puts( "Errors:\n#{errors.inspect}" )
end
statements = payload.dig( 'data', 'getStatements', 'statements', 'nodes' )
more_statements = payload.dig( 'data', 'getStatements', 'statements', 'pageInfo', 'hasNextPage' )
end_cursor = payload.dig( 'data', 'getStatements', 'statements', 'pageInfo', 'endCursor' )
statement_count += statements.size
else
STDOUT.puts( "Response code was #{response.code}:\n#{response.inspect}" )
end
end
STDOUT.puts( "Retrieved #{statement_count} statements." )