We Have To Go Deeper...

Grant Beasley · January 5, 2022

Welcome back and happy new year to all four (at last count) of my regular readers. We’ll continue where we left off; creating a GraphQL endpoint with Neo4j . This week we’ll go deeper and look at how we can call an API from within the GraphQL endpoint to enrich our data.

Enriching the Data

One part of the context that is lost when examining historical fixture data is historical weather data. In this post we’ll look at how we can use GraphQL Schema directives to apply custom logic on the server, which will allow us to use Cypher directly in the GraphQL schema file. Before all the exciting stuff though, we need to figure out how we’ll get the weather data in the first place. The best weather API I could find that was both free and had historical weather data was Meteostat and the process of getting a rapidapi key to access it was fairly straightforward. Once that was done, the first hurdle was finding the nearest weather station to each Stadium in the Neo4j database.

The query below uses apoc.load.jsonParams to pass the API key to the meteostat API and uses the latitude and longitude of the stadium to find the nearest weather station.

WITH {`Accept`: 'application/json',
    `Content-Type`:'application/json',
    `x-rapidapi-key`: <RAPID_API_KEY>
} as header
MATCH (f:Fixture)-[:VENUE]->(s:Stadium) WHERE f.fxid = 118011
WITH f,s,header
CALL apoc.load.jsonParams(
  'https://meteostat.p.rapidapi.com/stations/nearby?lat='+ s.latitude+
  '&lon='+s.longitude + '&limit5',
    header,
    null)
YIELD value WITH value as station
RETURN station


{
  "data": [
    {
      "name": {
"en": "Gloucestershire / Staverton"
      },
      "id": "EGBJ0",
      "distance": 6132.6
    },
    ...

Now we know how to access the data for the closest weather station, we can match all stadia and save the weather station ID to the graph for use in the GraphQL API.

WITH {`Accept`: 'application/json',
    `Content-Type`:'application/json',
    `x-rapidapi-key`: <RAPID_API_KEY>
} as header
MATCH (s:Stadium)
WITH s,header
CALL apoc.load.jsonParams(
      'https://meteostat.p.rapidapi.com/stations/nearby?lat='+
      s.latitude+'&lon='+s.longitude + '&limit5',
    header,
    null)
YIELD value WITH value as station,s
SET s.closestWeatherStation = station.data[0].id

Finding Historical Weather Data

We have the nearest station for each stadium so now we’re able to query the Meteostat API to find the weather for a given station on a given date/dates. The query below uses a similar query as above but adds some dates to the url in order to see what the output looks like.

WITH {`Accept`: 'application/json',
    `Content-Type`:'application/json',
    `x-rapidapi-key`: <RAPID_API_KEY>
} as header
MATCH (s:Stadium {name: 'Twickenham Stoop'})
WITH s,header
CALL apoc.load.jsonParams(
  'https://meteostat.p.rapidapi.com/stations/daily?station=' +
  s.closestWeatherStation + '&start=2020-01-01&end=2020-01-02',
    header,
    null)
YIELD value
RETURN value

{
  "data": [
    {
      "date": "2020-01-01",
      "wdir": 127.2,
      "wpgt": 20.4,
      "pres": 1029.3,
      "tmax": 6.8,
      "snow": null,
      "wspd": 7.0,
      "tavg": 5.3,
      "tmin": 1.7,
      "prcp": 0.0,
      "tsun": null
    },
    {
      "date": "2020-01-02",
      "wdir": 188.6,
      "wpgt": 40.8,
      "pres": 1020.4,
      "tmax": 10.8,
      "snow": null,
      "wspd": 18.4,
      "tavg": 8.7,
      "tmin": 6.2,
      "prcp": 0.0,
      "tsun": null
    }
  ],
  "meta": {
"generated": "2022-01-05 21:33:12"
  }
}

As we can see above, the API call returns a list of maps, each containing various measurements of the weather on a given date.

@cypher Directives and Type Definitions

We can apply custom logic on the server to return scalar values, nodes, objects and in this case maps. Scalar values and nodes are relatively simple as the type definitions have already been defined in the schema file. In our example, we’re returning a map of weather conditions, a type that hasn’t yet been defined in the GraphQL schema file.

The first step then is to create a type in our schema.graphql file that maps the the output of our weather values to some type definitions.


// Step 1 - create a Weather type to store
// the weather map

type Weather {
        windDirection: BigInt
        wpgt: BigInt
        pressure: BigInt
        maxTemp: BigInt
        snow: BigInt
        windSpeed: BigInt
        avgTemp: BigInt
        minTemp: BigInt
        precipitation: BigInt
        tsun: BigInt
}

Now the exciting stuff, calling an API from within an API, all very Inception like if you ask me.

We can use the @cypher directive to apply the custom logic that extends the Fixture type and returns a Weather type for a given fixture. The custom logic takes the date of a fixture, the weather station nearest to the fixture’s venue, passes this to the Meteostat API and finally returns a representation of a Weather object.

extend type Fixture {
        fixtureConditions: Weather @cypher(statement:"""
        WITH {
            `Accept`: 'application/json',
            `Content-Type`:'application/json',
            `x-rapidapi-key`: <RAPID_API_KEY>
         } as header, this
        MATCH (this)-[:VENUE]->(s:Stadium)
        WITH this, s, header
        CALL apoc.load.jsonParams(
          'https://meteostat.p.rapidapi.com/stations/daily?station=' +
           s.closestWeatherStation + '&start=' + this.fixtureDate + '
           &end=' + this.fixtureDate,
        header,
        null)
        YIELD value WITH value.data[0] as weather
        RETURN {
            windDirection: weather.wdir,
            wpgt: weather.wpgt,
            pressure: weather.pres,
            maxTemp: weather.tmax,
            snow: weather.snow,
            windSpeed: weather.wspd,
            avgTemp: weather.tavg,
            minTemp: weather.tmin,
            precipitation: weather.prcp,
            tsun: weather.tsun
        } as conditions""")
}

And just to prove that this thing actually works, here’s an example of a GraphQL query for a fixture which contains fixture teams, scores and weather details. Voila!


query fixtureQ {
  fixtures(where: {fxid: 120081}) {
    HTFTSC
    ATFTSC
    fixtureConditions {
      windSpeed
      windDirection
      avgTemp
      maxTemp
      minTemp
      precipitation
      pressure
      snow
      tsun
      wpgt
    }
    teamsTeamPlayedInConnection {
      edges {
        node {
          name
        }
        home_away
      }
    }
  }
}


{
  "data": {
    "fixtures": [
      {
        "HTFTSC": "48",
        "ATFTSC": "10",
        "fixtureConditions": {
          "windSpeed": "16.5",
          "windDirection": "268.4",
          "avgTemp": "6.7",
          "maxTemp": "10",
          "minTemp": "4",
          "precipitation": "1.4",
          "pressure": "1020.4",
          "snow": null,
          "tsun": null,
          "wpgt": "33.3"
        },
        "teamsTeamPlayedInConnection": {
          "edges": [
            {
              "node": {
                "name": "Sale"
              },
              "home_away": "home"
            },
            {
              "node": {
                "name": "Harlequins"
              },
              "home_away": "away"
            }
          ]
        }
      }
    ]
  }
}

Twitter, Facebook