Agrimetrics GraphQL API Introduction
Agrimetrics GraphQL API Introduction
Table of Contents
Introduction To GraphQL {#introduction-to-graphql}
What is GraphQL?
GraphQL (Graph Query Language) is a query language for graph-like data. You can use the Agrimetrics GraphQL API to query Agrimetrics data.
GraphQL APIs are backed by a machine-readable schema and human-readable documentation which can be used by user interfaces and tools to help explore and explain the graph and its data.
You can find a general introduction to GraphQL at https://graphql.org/learn/.
You can explore our data, and try out queries, using our Graph Explorer application — note that this application requires that you hold an Agrimetrics Field Explorer license (full or trial).
If you have any questions or feedback, please contact us.
Agrimetrics Graph API {#agrimetrics-graph-api}
A simple example
A GraphQL query is sent to a server via an HTTP POST
request; here's a simple example - asking for the area of a field.
Here is the query:
query {
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
area {
value
unit
}
}
}
Here's how we would send it to the GraphQL API using curl
(assuming your API key is stored in a shell variable called API_KEY
):
curl -XPOST -H 'content-type: application/json' -H "ocp-apim-subscription-key: ${API\_KEY}" https://api.agrimetrics.co.uk/graphql/v1 -d '{"query": "query { fields(where: {id: {EQ: \\"agfd:C6BgTxUxhMG\_OCGrLGW8qw\\"}}) { area { value unit }}}"}'
Here is the result:
"data": {
"fields": [{
"id": "https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG\_OCGrLGW8qw",
"area": {
"value": 9.9364,
"unit": "http://data.agrimetrics.co.uk/units/hectares"
}
}]
}
}
See below for more detail, see Examples for more examples.
Licensing{#licensing}
The GraphQL API (and the Graph Explorer application) requires a Field Explorer subscription. Trial subscriptions may access the Graph Explorer and API, but functionality will be limited. Some data sets (e.g. Field Boundaries and Crop Observations) require additional subscriptions or payments beyond a standard Field Explorer subscription.
The API will return an informative error if you try to access something beyond your current subscription.
Calling the API{#calling-the-api}
Every GraphQL request is a POST
request to the same endpoint, https://api.agrimetrics.co.uk/graphql/v1
, with the GraphQL query text contained in the body of the request. Your subscription key should be passed in the Ocp-APIM-Subscription-Key
header.
Here is the simple example above in more detail:
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
area {
value
unit
}
}
}
We are querying for a field by its known id
, and requesting the area
of the field. The concept area
itself contains multiple properties, including the value
and unit
which are requested.
The where
parameter uses a comparator expression to filter the results. You will find the where
pattern throughout the Agrimetrics GraphQL API. Depending on what is being queried, different properties will be available for filtering, with various comparator operators available. In this example we filter the id
property using the equality operator, EQ
.
This could be sent to the API using the following curl
command line (assuming your API key is stored in a shell variable called API_KEY
):
curl -XPOST -H 'content-type: application/json' -H "ocp-apim-subscription-key: ${API\_KEY}" https://api.agrimetrics.co.uk/graphql/v1 -d '{"query": "query { fields(where: {id: {EQ: \\"agfd:C6BgTxUxhMG\_OCGrLGW8qw\\"}}) { area { value unit }}}"}'
Note that the GraphQL query text is encoded as a string in the query
property of a JSON object, with quotes in the query text escaped using back-slashes. White-space in the query text is not considered significant, thus the query may be expressed in one line without line-feeds or indentation.
The easiest way to export a query for use in
curl
, Python or other tools is to use the Graph Explorer's "Export Code" feature.
Response
A request made with a valid GraphQL syntax will always return a 200 OK
response code, with a JSON object as the body (unless instructed otherwise).
The request above returns the following JSON object response body:
"data": {
"fields": [
{
"area": {
"value": 9.9364,
"unit": "http://data.agrimetrics.co.uk/units/hectares"
}
}
]
}
}
All the data we requested is returned under the data
property, in a similar structure to that of the query.
Because the underlying schema declares the value of fields
as a list, the API returns an array of field data objects, in this case, with only one value. If no fields matched your query, the fields
array would be empty.
Errors
In case of syntax errors in the GraphQL query, the server will return a 400 Bad Request
response, otherwise the server will always return a 200 OK
response, even if errors happen when actually evaluating the query; both responses will have a JSON object body.
In case of either type of error, there will be an errors
property (a JSON array) at the top level of the JSON object. There may still be a data
property if any part of the query evaluated correctly, otherwise data
will not be present.
Depending on your use case, you may choose to discard the data
if there were any errors at all, or you may choose to use any data that was returned, and merely report, or even ignore the errors.
Here's an example of a response to a request for the area
and shape
(boundary) of a field, when the subscription doesn't have sufficient credits to retrieve boundaries:
{
"errors": [
{
"message":"Insufficient credits to complete request",
"locations":[{"line":1,"column":69}],
"path": ["fields",0,"shape"],
"extensions":{"code":"PAYMENT\_REQUIRED"}
}
],
"data": {
"fields": [
{
"area": {
"value": 9.9364,
"unit": "http://data.agrimetrics.co.uk/units/hectares"
},
"shape":null
}
]
}
}
The area
property is returned successfully as before, but shape
is null
, and there is now an errors
array at the top level, with details about the error, including the path
which shows on which property of the query the error occurred, and an informative message
and code
.
Output Formats{#output-formats}
JSON
By default the GraphQL API returns results as JSON.
All URIs returned from the GraphQL API are compatible with the Agrimetrics JSON-LD REST APIs.
If you are interested in JSON-LD output, please contact us for details.
Tabular
By specifying an Accept
header of text/csv
in your query, the API can return data in tabular format. This is currently an experimental feature.
If returning tabular, we strongly recommend that you keep your queries simple, in particular these queries should not contain two or more data series at the same level.
If there are errors when evaluating a query that is requested in tabular format, a non-200 HTTP error status will be returned with a JSON body containing error information.
Data Sets{#data-sets}
The data returned by the GraphQL API comes from a variety of Data Sets. By specifying the dataSetId
property in your query you can see the source Data Set for a specific type of data such as a weather observation:
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
id
weatherObservations(where: {date: {GE: "2020-01-01", LT: "2020-02-01"}}) {
rainfallTotalDaily {
value
dataSetId
}
}
}
}
returns
{
"data": {
"fields": [
{
"id": "https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG\_OCGrLGW8qw",
"weatherObservations": {
"rainfallTotalDaily": [
{
"value": 0,
"dataSetId": "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be"
}
]
},
...
}
]
}
Note that in this example, and others throughout this documentation, the inclusion of
...
in the JSON response simply indicates this is a list of results and there would be more data in a real response.
Where there are multiple possible Data Sets for a given type of data, the API will return data from a default. It is also possible to specify the Data Set you wish to use by specifying dataSetId
in a where
clause:
query {
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
id
weatherObservations(where: {date: {GE: "2020-01-01", LT:"2020-02-01"}}) {
rainfallTotalDaily(where: {dataSetId:{EQ: "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be"}}) {
value
dataSetId
}
}
}
}
returns
{
"data": {
"fields": [
{
"id": "https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG\_OCGrLGW8qw",
"weatherObservations": {
"rainfallTotalDaily": [
{
"value": 0,
"dataSetId": "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be"
},
...
]
}
}
]
}
}
Note that when you limit the Data Set using where.dataSetId
, if there is no data available, e.g. for your given date range, you will receive an empty list of results.
You can find out information about a Data Set via its Id using a node
query:
query {
node(id: "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be") {
id
... on DataSet {
id
title
description
}
}
}
Full information for all Data Sets is also available in the Data Catalogue.
A Note on Ids{#a-note-on-ids}
All node entities in the graph have an id
property, which will be returned as a full URI, for example https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG_OCGrLGW8qw
.
When querying by id
the full URI can be provided, or alternatively, there exist convenience short-hand prefixes for many entities.
The above field can be resolved equivalently using agfd:C6BgTxUxhMG_OCGrLGW8qw
as the id
.
Below is a list of prefixes, the concept they represent, and the full URI they will evaluate to:
Prefix | Concept | Equivalent URI |
---|---|---|
agfd | field | https://data.agrimetrics.co.uk/fields/ |
agap | agricultural-plant | https://data.agrimetrics.co.uk/ontologies/agricultural-plant/ |
agsl | soil-layer | https://data.agrimetrics.co.uk/soil-layers/ |
agcat | data-set | https://data.agrimetrics.co.uk/data-sets/ |
Advanced GraphQL Concepts{#advanced-graphql-concepts}
The following information will help you to make full use of the power of GraphQL in complex queries and when asking for large amounts of data.
Parameterizing Queries{#parameterizing-queries}
Instead of substituting values such as coordinates or dates directly into a query text, you can use variables in your query, and pass the values separately. This is more secure and less error-prone. It is strongly recommended to use parameterized queries instead of attempting to achieve this with either string concatenation or interpolated strings.
For more information, see the Variables documentation in the GraphQL learning resource.
Cursors{#cursors}
To obtain all the results of a large data set, you will need to use cursors.
Cursors allow you to efficiently retrieve a page of results with each request, and to progress through subsequent pages until all data has been returned, even if the total data to be retrieved is very large.
See our documentation on cursors for details of how to apply them in your queries.
Query Fragments{#query-fragments}
Sometimes a query will include lots of repetition, for instance getting the same properties from multiple nodes, where the underlying type of the node is the same; using query fragments can minimise how much duplication these queries need.
To see how you can use fragments to make complex queries easier to manage, read our documentation on query fragments, and the Fragments section in the offical GraphQL learning resource.
Interfaces and Union Types{#interfaces-and-union-types}
GraphQL schemas allow defining interfaces and union types.
Interfaces are abstract types that define a set of properties. Any type that includes these properties can then implement that interface.
Union types are effectively a set of distinct types. If a property is a union-type, data returned will be one of the set of types defined.
We use these concepts in our GraphQL API, and they can be used with Query Fragments and the special node
top-level query.
More information on interfaces and union types can be found in the GraphQL documentation.
Examples{#examples}
Simple Examples{#simple-examples}
The examples shown earlier as an introduction to GraphQL show querying a field by its known ID
. Fields can also be found by geo-location, using either a Point
, where the point must lie within a field:
query {
fields(geoFilter: {location: {type: Point, coordinates: [0.089372, 52.245591]}}) {
id
}
}
Note that
Point
is[lon, lat]
as per GeoJSON.
or a Point
with a distance
parameter to specify a radius (in metres) of a circle that the field must intersect with:
fields(geoFilter: {distance: {LE: 5000}, location: {type: Point, coordinates: [0.089372, 52.245591]}}) {
id
}
}
or a Polygon
to specify a shape that the field must intersect with:
query {
fields(geoFilter: {location: {type: Polygon, coordinates: [[[\-0.63720703125, 51.09144802136697], [\-0.582275390625, 51.057796900048594], [\-0.53009033203125, 51.076782592536524], [\-0.509490966796875, 51.11473061746101], [\-0.53558349609375, 51.13972478986592], [\-0.6042480468749999, 51.128522178293224], [\-0.63720703125, 51.09144802136697]]]}}) {
id
}
}
The
location
parameter here is a GeoJSON Polygon.
or, finally, a list of field ids, using the where
parameter, instead of a geoFilter
:
query {
fields(where: {id: {EQ: ["agfd:C6BgTxUxhMG\_OCGrLGW8qw", "agfd:-8Ux5v46seUPmyjqH3-5Sw"]}}) {
id
}
}
Return the Weather Forecast, for a given field, generated on a given date.
{
query {
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
weatherForecast(where: {generatedAt: {EQ: "2019-01-01"}}) {
temperatureMeanDaily {
dateTime
value
}
rainfallTotalDaily {
dateTime
value
}
windSpeedMeanDaily {
dateTime
value
}
}
}
}
returns
{
"data": {
"fields": [
{
"weatherForecast": {
"temperatureMeanDaily": [
{
"dateTime": "2019-01-01T00:00:00.000Z",
"value": 5.88
},
...
],
"rainfallTotalDaily": [
{
"dateTime": "2019-01-01T00:00:00.000Z",
"value": 0
},
...
],
"windSpeedMeanDaily": [
{
"dateTime": "2019-01-01T00:00:00.000Z",
"value": 4.364
},
...
]
}
}
]
}
}
Return a specific 3 months of total rainfall for a given field
query {
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
weatherObservations(where: {date: {GE: "2019-03-01", LT: "2019-06-01"}}) {
rainfallTotalDaily {
value
dateTime
}
}
}
}
returns
{
"data": {
"fields": [
{
"weatherObservations": {
"rainfallTotalDaily": [
{
"value": 0,
"dateTime": "2019-03-01T00:00:00.000Z"
},
{
"value": 0.2,
"dateTime": "2019-03-02T00:00:00.000Z"
},
...
]
}
}
]
}
}
Advanced Examples{#advanced-examples}
Cursoring over large result-sets{#cursoring-over-large-result-sets}
Retrieving a large set of data requires using a cursor. This example uses parameterization, however this is optional.
query($cursor: String) {
fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
weatherObservations(
where: {
date: {
GE: "2018-03-01",
LE: "2019-01-01",
}
}
after: $cursor
) {
cursor
rainfallTotalDaily {
value
dateTime
}
}
}
}
A first request, with cursor
set to null
returns the first set of data:
{
"data": {
"fields": [
{
"weatherObservations": {
"cursor": "MjAxOC0wOC0wMw==",
"rainfallTotalDaily": [
{
"value": 1,
"dateTime": "2018-03-01T00:00:00.000Z"
},
{
"value": 2.7,
"dateTime": "2018-03-02T00:00:00.000Z"
},
...,
{
"value": 0,
"dateTime": "2018-08-03T00:00:00.000Z"
}
]
}
}
]
}
}
Note that the returned cursor,
MjAxOC0wOC0wMw==
, is not null. Thus there is additional data which can be retrieved with subsequent requests.
Repeating the query, but this time with the variable cursor
set to MjAxOC0wOC0wMw==
returns:
{
"data": {
"fields": [
{
"weatherObservations": {
"cursor": null,
"rainfallTotalDaily": [
{
"value": 0,
"dateTime": "2018-08-04T00:00:00.000Z"
},
{
"value": 0,
"dateTime": "2018-08-05T00:00:00.000Z"
},
...
]
}
}
]
}
}
These results begin where the last request left off.
In the original request, the last dateTime
provided was '2018-08-03T00:00:00.000Z', the first result of the second request has a dateTime
of '2018-08-04T00:00:00.000Z'.
An implementation of this can be seen in the examples.
Return the Soil Texture for every field in a given area{#return-the-soil-texture-for-every-field-in-a-given-area}
In this example, we obtain the top-soil and sub-soil texture of all the fields within 10,000 m of a point specified by a latitude and longitude.
If there are a large number of fields, it is important to request the cursor to ensure you get all the data, for example the following query:
{
fields(geoFilter: {distance: {LE: 10000}, location: {type: Point, coordinates: [0.089372, 52.245591]}}) {
cursor
id
soil {
subSoil {
texture {
clayPercentage
sandPercentage
siltPercentage
type
}
}
topSoil {
texture {
clayPercentage
sandPercentage
siltPercentage
type
}
}
}
}
}
returns
{
"data": {
"fields": [
{
"cursor": "MQ==|aHR0cDovL2RhdGEuYWdyaW1ldHJpY3MuY28udWsvZmllbGRzLy0wVmRaZHlvMnQya0dhRUlYWlM2Z0E=",
"id": "https://data.agrimetrics.co.uk/fields/-0VdZdyo2t2kGaEIXZS6gA",
"soil": {
"subSoil": {
"texture": {
"clayPercentage": 24.67,
"sandPercentage": 49.67,
"siltPercentage": 25.67,
"type": "SANDY\_CLAY\_LOAM"
}
},
"topSoil": {
"texture": {
"clayPercentage": 24.69,
"sandPercentage": 47.88,
"siltPercentage": 27.43,
"type": "SANDY\_CLAY\_LOAM"
}
}
}
},
{
"cursor": "NTAw|aHR0cDovL2RhdGEuYWdyaW1ldHJpY3MuY28udWsvZmllbGRzLzl6MC1ibmZRNmlaTF8yV2FxRXdrRXc=",
"id": "https://data.agrimetrics.co.uk/fields/9z0-bnfQ6iZL\_2WaqEwkEw",
"soil": {
"subSoil": {
"texture": {
"clayPercentage": 24.75,
"sandPercentage": 47.49,
"siltPercentage": 27.76,
"type": "SANDY\_CLAY\_LOAM"
}
},
"topSoil": {
"texture": {
"clayPercentage": 23.69,
"sandPercentage": 44.64,
"siltPercentage": 31.67,
"type": "LOAM"
}
}
}
},
...
]
}
}
Note in this request every field has a cursor. You can continue a query starting from any of these fields.
If the last cursor is null, there are no more fields to cursor over.
Updated about 1 year ago