Flights Search Application with Neo4j — GRANDstack and GraphQL Custom Resolver (Part 3)

GRANDstack overview: Neo4j Database, React, GraphQL API on Apollo server powered by neo4j-graphql-js library

Vlad Batushkov
Neo4j Developer Blog
9 min readApr 20, 2020

--

In this series of articles, I share my experience of building a web application that you can use to search for flights. The project was started for Neo4j Workshop in Bangkok, November 5, 2019. I hope this series can help you in your learning journey of modern web development tools using Neo4j, Docker and GraphQL. This article covers the last phase of project development: applying GRANDstack framework. I will share how easy it can be to use the GRANDstack toolset to build a full-stack web application. Project GitHub repo with all source code you can find here. Previous parts of this series:

Target

The general idea of Flights Search Application is to have a web page with a search box, where you can define a place of departure and destination, choose the date and voila — a list of available flights would appear below. Our needs are limited by only a one-way flight, no class options, and no other fancy options. A brutal one-way flight for MVP.

Very basic Flights Search bar

Architecture

Before jumping to the front-end, let’s briefly overview an overall design of an existing Flights Search Application. The database is provided as a Docker image, including 1 month of daily Flights scheduler of Airports all over the world from openflights.org (data was modified to Neo4j-friendly imports with my small dotnetcore console app). A detailed explanation of how to build your own Neo4j Docker Image with Database import using the neo4j-admin import tool is in my first part of this series.

Architecture for the Flights Search application

I already wrote a query to find flights. From the first view, it may look like an ugly monster, but, don’t worry, the query is not as complex as it looks.

The query is explained in the article how to write your own APOC Custom Procedures & set it up in Docker using Cypher-shell. The database Schema is represented below (not totally the same, but 99%).

Max De Marzi original content. Flight Search Proof of Concept Database Schema.

GRANDstack

Now let’s move forward. First, a few words about the general idea of GRANDstack, a rock-star mix of modern technologies to quickly build a full-stack application.

It’s no secret that graphs drive modern development and the latest technology in this area is GraphQL. GraphQL APIs are aggressively replacing REST API endpoints in many companies.

Why not use a native graph database like Neo4j with graph-oriented GraphQL queries? It sounds like a really smart idea. Having any UI framework aware of GraphQL, we’ll get a full-funnel graph-oriented solution.

GRANDstack is a framework that combes these technologies into one piece.

GraphQL + React + Apollo + Neo4j Database = GRANDstack

I want to point out, that React as “R”-component, actually can be replaced with any other client-side library or framework: Angular, Vue, Elm or whatever you want. The same is true for Apollo server, with some other options to replace “A”.

From my point of view, the fundamental “component” of GRANDstack is a neo4j-graphql-js library, that translates GraphQL query to Cypher and connects “G” GraphQL with “ND” Neo4j Database.

Web Application

Flights Search Application requires mostly super simple stuff on the front-end side. GRANDstack makes development easier than you can imagine.

From the GitHub GRANDstack starter project, we can fork or copy a bootstrap of our future Application and implement Flights Search on top of it.

How many pages do we need in the Flights Seach App?

  1. Grid-view pages: list all entities of Airports and Airlines.
  2. Map page: beauty world map to visualize Airport connections.
  3. Search for Flights: the main page with a search bar and list of result flights.

Now, one by one.

Grid page

Let’s build a grid-view page. The easiest page.

Airports table view

I can totally reuse the provided React List page as a template for my Airports (as well for Airlines). GraphQL schema definition for type Airport will be totally based on the Airport node structure from Neo4j Database.

Compare GraphQL Airport Type definition to some Airport Node in Neo4j Database

The definition of Airport type has a “location” property of type Point. This type, provided by neo4j-graphql-js and gives us the ability to work with Spatial data in a simple and short way (more about Spatial Type in docs).

Really impressive, how many other different useful types neo4j-graphql-js library gives us absolutely for free. You can scroll Schema definitions and find various other types. For example, here we have all possible Types for grid use-cases: filtering, sorting, pagination.

Use-case for filter, orderBy, first

Grid page is nothing more than just a representation of a list of Airports. React component Query from react-apollo library helps us to request the data.

Filtering a table by city name “Moscow” will translate GraphQL into the following Cypher query:

MATCH (`airport`:`Airport`) WHERE (`airport`.code CONTAINS $filter.code_contains) AND (`airport`.country CONTAINS $filter.country_contains) AND (`airport`.city CONTAINS $filter.city_contains) WITH `airport` ORDER BY airport.name ASC RETURN `airport` { .code , .name , .city , .country ,location: { longitude: `airport`.location.longitude , latitude: `airport`.location.latitude }} AS `airport` LIMIT toInteger($first)
{
"offset": 0,
"first": 100,
"filter": {
"code_contains": "",
"country_contains": "",
"city_contains": "Moscow"
}
}

Map page

Here we will use the power of GraphQL augmented schema to showcase some Cypher-related features. Here is the page with a world map to visualize how departure Airport is connected to arrival Airports:

From Sheremetyevo Airport to any place in the world (almost)

The UI implementation of this page is based on React Simple Maps. I will not focus on this topic in the article.

From the GRANDstack perspective, there are 2 properties in GraphQL schema for Airport Type:

  1. directs” — list of all Airports connected to this Airport with Direct Flight.
  2. neighbors” — list of Airports in the same city as this Airport. But not this Airport itself.

Directs

The “directs” property is built with the help of the relation directive. In Neo4j Database all Airports connected to each other with [FLIES_TO] relationship. We can simply declare this relationship in GraphQL schema, request a field and enjoy the data, provided by automatically generated Cypher query.

SVO — [FLIES_TO] → Airports (only shows 25 items from 143 total)

Neighbors

The “neighbors” property represents a relationship that does not exist in the Neo4j Database, so there is no way to use relation directive. To solve this problem we use the cypher directive. It allowed us to create our own Cypher query and resolve this property of GraphQL schema.

MATCH (a:Airport) WHERE this.city = a.city AND this <> a RETURN a

Search page

Flights Search functionality is implemented as a Neo4j Custom Procedure and now I want to call it using the GraphQL resolver and map the result object to the GraphQL schema.

Let’s look at the structure of the Procedure result. In the example below a single item look like this:

[{
"flights": [
{
"flight": {
"flight_number": "SU_BKK_SVO_20200101",
"price": 12800,
...
},
"company": {
"name": "Aeroflot Russian Airlines",
"code": "SU",
"country": "Russia"
}
}
],
"route": [
{
"code": "BKK",
"name": "Suvarnabhumi Airport",
"city": "Bangkok",
"country": "Thailand"
},
{
"code": "SVO",
"name": "Sheremetyevo International Airport",
"city": "Moscow",
"country": "Russia"
}
],
"stops": 0
}, ...]

An array of objects with more inherited array objects. JSON of Neo4j Types:

[{
flights: [
{
flight: { Flight },
company: { Airline }
}
],
route: [
{ Airport }
],
stops: Int
}, ...]

The main complexity of this result — it contains objects, that does not match to any existed Nodes and Relationships of Neo4j Database.

The challenge is how to make this JSON result properly map to the GraphQL schema.

Try to use cypher directive

I tried to use existing functionality and call a Custom Procedure using cypher directive. Automatic object mapping, unfortunately, this didn’t work.

Disclaimer: In the last section, I will show my own way to solve this issue. But I would gladly look into an elegant solution using less manual work. If you faced the same issue and alredy know fast and the furious solution, please, share in comments. My approach is described only in learning purposes.

GraphQL Custom Resolver and Mapping

Instead of using cypher directive, I do a manual database call by using a GraphQL Custom Resolver. The resolver does a call of the Custom Procedure using Neo4j javascript driver.

But now all returning types are Neo4j Types and I lost all the magic of auto-mapping from neo4j-graphql-js library. Javascript driver returns objects of Neo4j Types, not GraphQL Types.

Lesson learned: neo4j-graphql-js does a great job, by translating Neo4j Types into GraphQL Types (for example, _Neo4jDateTime and _Neo4jPoint ). Without neo4j-graphql-js mapping, we need to write it by ourselves. Let’s understand, how painful is this.

Cypher Map projection

First of all, we can try Type mapping on the Neo4j side. From Neo4j Type to base GraphQL primitive types (existed scalar types).

Let’s get rid of Node types, they are pretty bulky. A possible way to translate Nodes into plain JSON objects is to use Cypher map projection: x { .* }

Before, getFlightsNodes returns Nodes

...
WITH collect({ stops: 2, route: list.route, flights: [{ flight: f1, company: a1 }, { flight: f2, company: a2 }, { flight: f3, company: a3 }] }) as transfers, transfer, direct
...

After, getFlightsObjects returns plain JSON objects

...
WITH collect({ stops: 2, route: list.route, flights: [{ flight: f1 { .* }, company: a1 { .* } }, { flight: f2 { .* }, company: a2 { .* } }, { flight: f3 { .* }, company: a3 { .* } }] }) as transfers, transfer, direct
...

This projection helped to get rid of Nodes, but we still have property Types to map: Int, DateTime, Point.

Scalar Types

Another technique to try is using custom GraphQL Scalar Types. For example, we have a DateTime Type structure as a result of the javascript driver.

I want to have something much more simple in my GraphQL schema. So I can declare custom FlightsDataTime and FlightsInt as GraphQLScalarTypes.

The GraphQL API is ready to handle the new FlightsSearchObjects query.

The main goal achieved: the JavaScript driver returns a readable result from the Custom Procedure and is properly processed by GraphQL (thankful to Cypher Map projection and GraphQL Scalar Types).

Is it a perfect solution? Frankly speaking, I don’t think so… But exploring the process was fun. Now we know a little bit more about the core stuff.

Flights Search page works! The procedure analyzes thousands of paths between Airports in seconds and shows to user details about flights. Pretty good result for a pure Neo4j-based solution.

End of Part 3. End of series. Conclusion

That’s it! It was a long trip. The workshop event was hosted in November 2019 and the last part of the article series finally published in April 2020. Wow.

A lot of things in implementation were changed compared to what was discussed and showed on that Workshop. A lot. Every step of work was revisited and improved. Fork the Flights Search GitHub repo to explore the project and share your opinion.

For every article, I provided a deep dive into details of implementation with problems, solutions, and workarounds I faced. It was hard work. This is why I really believe, that this educational project can help other developers like me to save time and made a correct decision.

Thanks for reading. And enjoy work with Neo4j Database.

Cheers!

--

--

Vlad Batushkov
Neo4j Developer Blog

Engineering Manager @ Agoda. Neo4j Ninja. Articles brewed on modern tech, hops and indie rock’n’roll.