How I built… the Neosemantics (n10s) Graph App

Explaining how I put together a Graph App in just a few days using open-source components.

Adam Cowley
May 15, 2020 · 8 min read

In this case, I was looking to build a Graph App to help educate users about Neosemantics, or n10s. Neosemantics is a Neo4j Labs project that allows you to store RDF/Linked Data in Neo4j and then export the data back out in a lossless way.

The idea behind the Graph App was to provide an educational tool that would allow you to preview how your triples would look in Neo4j without you having to know much about the underlying technology and also to teach more experienced users how the procedure API of the library actually works.

If you want to install the app into Neo4j Desktop test it while we look at the code, just open the “Graph App Gallery” or go to install.graphapp.io and click the install button.

Graph App Gallery with the neosemantics app

If all you care about is code, skip to the TL;DR at the bottom of the post…

Yeah, but what are Graph Apps?

Graph Apps are Single Page Applications (built with HTML/Javascript and optionally the front-end framework of your choice) that are installed to and hosted by Neo4j Desktop.

The Graph App Gallery has a comprehensive list of apps built by Neo4j, our Partners and even Community Members. Two Graph Apps that I use on a regular basis are Halin, a monitoring & management tool and the Query Log Analyser which gives you a more user friendly view on your query logs.

Neo4j Desktop provides you with an API that gives you access to the User’s Projects and Graphs, along with Files and Activation Keys. From here, you can run a reduce/filter on the projects and graphs to find the active graph.

const context = await neo4jDesktopApi.getContext()
const activeGraph = context.projects
.reduce((acc, project) => acc.concat(project.graphs), [])
.filter(graph => graph.status === 'ACTIVE')

From there you can find the server URL and credentials and create an instance of the driver and run cypher statements against the graph.

const { url, username, password } = activeGraph.connection.configuration.protocols.bolt// Create a driver instance
const driver = new neo4j.driver(
url, // Neo4j URL - bolt://…
neo4j.auth.basic(username, password) // Auth
)

Making use of Open Source Components

As I touched upon before, as long as your code boils down to a html file and some javascript files the choice of framework is moot. Almost everything built by the Neo4j engineering team is built using React but I’m personally more of a Vue.js kind of guy.

Vue.js comes with a CLI tool that allows you create projects from pre-made templates, so with a single command you can generate the project structure:

vue create neosemantics

Vue & Neo4j

A while back while I was building these apps for clients as my day job, I put together a Vue plugin to simplify interactions with Neo4j. The vue-neo4j plugin adds a $neo4j object to all components which contains helper functions for connecting to the database and running queries.

// Something.vue
export default {
name: 'something',
// ...
data: () => ({
driver: false,
protocol: 'bolt',
host: 'localhost',
port: 7687,
username: 'neo4j',
password: 'trustno1'
}),
methods: {
connect() {
this.$neo4j.connect(
this.protocol,
this.host,
this.port,
this.username,
this.password
)
}
},
computed: {
driver() {
return this.$neo4j.getDriver()
}
},
}

If the plugin detects the Neo4j Desktop API’s, it will also provide you with a driver already instantiated with the connection details of the active graph.

One useful component that this plugin includes is the vue-neo4j-connect component. Most people will need a login form for their app, so this component provides you with everything you need.

Opening the project in Neo4j Desktop will show a list of Projects and Graphs in Neo4j Desktop but will fall back to a generic login form if opened in a browser.

When the app is opened in Neo4j Desktop, the user will be given a list of projects and graphs to connect to along with a button to connect to the current active graph. If the user clicks Connect to another graph or the app is opened in a browser a generic login form where the user fills out the Neo4j credentials manually.

Look and Feel

Anyone with a keen eye will also notice that a whole host of Neo4j products including Neo4j Desktop, Neo4j Browser and Neo4j Bloom use some sort of variation on top of Semantic UI so to make the n10s Graph App look somewhat similar, it would make sense to use that too.

There is also a Vue plugin for Semantic UI called semantic-ui-vue. Each component is prefixed with sui- and with a quick search of the documentation you can quickly find what you’re looking for. Here is an abridged version of the config form:

<sui-form>
<sui-form-field>
<label>handleVocabUris</label>
<sui-dropdown
fluid
:options="handleVocabUriOptions"
placeholder="handleVocabUris"
search
selection
v-model="handleVocabUris"
/>
</sui-form-field>
<sui-button primary
:loading="loading"
@click.prevent="runQuery"
>
{{ buttonText }}
</sui-button>
</sui-form>

This also sped up the development process by providing a set of pre-made components for everything from basic layout to form elements. This also had the benefit of taking most of the design decisions out of my hands.

So with @vue/cli and two open source components, I was able to build a large part of the Graph App relatively quickly. The look and feel was taken care of by Semantic UI, and the interaction with the Neo4j Database would go through vue-neo4j.

Generic Components

Many pages in the UI also made use of generic components. For example, all of the Cypher result tables use the same component. The component simply takes the result provided from the neo4j-driver and displays the results within an <sui-table> component using a v-for loop.

<template>
<div class="n10s-result">
<sui-table compact striped v-if="!noResults && isTable">
<sui-table-header>
<sui-table-header-cell v-for="key in headers"
:key="key">
{{ key }}</sui-table-header-cell>
</sui-table-header>
<sui-table-body>
<sui-table-row v-for="(record, index) in
result.records" :key="index">
<sui-table-cell v-for="key in headers"
:key="index+key">
{{ record.get(key) }}
</sui-table-cell>
</sui-table-row>
</sui-table-body>
</sui-table>
<graph-result v-if="!noResults && !isTable"
:result="result"
/>
<p class="ui tiny" v-if="summary" v-html="summary" />
</div>
</template>

The component itself takes the result as a prop and then makes use of the keys property on the first row to create the header row as a computed property, then iterates through result.records to display the actual results of the query.

export default {
name: 'n10s-result',
props: {
result: Object,
displayAs: {
type: String,
default: 'table',
},
},
components: {
GraphResult,
},
computed: {
isTable() {
return this.displayAs === 'table'
},
noResults() {
return this.result && !this.result.records.length
},
headers() {
return this.result.records[0].keys
},
summary() {
const consumed =
this.result.summary.resultConsumedAfter.toNumber()
const available =
this.result.summary.resultAvailableAfter.toNumber()
return `
Started streaming ${this.result.records.length}
record${this.result.records.length === 1 ? '' : 's'}
after ${available}ms
and completed after ${consumed}ms
`
},
},
}

That component is then capable of handling anything that is thrown at it…

Generic components are a great way to speed up development

Where a Forced Graph layout is required, I switched out the Result component for another component which wraps the Vis.js network library.

Many of the components also run a Cypher statement against the database. Rather than duplicating this code many times, you can create a Mixin. Mixins allow you to define functionality that can be inherited by importing the object and listing it to the mixins array on the component.

export default {
data: () => ({
loading: false, error: false, result: false, tab: 0
}),
computed: {
cypher: () => 'MATCH (n) RETURN count(n) AS count',
params: () => {},
},
methods: {
runQuery() {
this.loading = true
this.error = false
this.$neo4j.run(this.cypher, this.params)
.then(res => this.result = res)
.catch(e => this.error = e)
.finally(() => {
this.tab = 1
this.loading = false
})
},
},
}

In this case, I’ve defined cypher as a computed variable but defining this within a component that uses this mixin that contains its own cypher variable will overwrite this. That way, if I forget to overwrite the variable I won’t get any errors in the console.

import CypherComponent from './mixins/CypherComponent'export default { 
name: 'import',
mixins: [ CypherComponent ],
// ...
computed() {
cypher() {
return `CALL n10s.rdf.import.fetch(
"${this.url}",
"${this.format}",
${JSON.stringify(this.parameters)}
)`
},
},
}

The TL;DR Process

Use @vue/cli to generate a new project.

vue create neosemantics

Install vue-router for navigation, and vue-neo4j and semantic-ui-vue as dependencies.

npm install --save vue-router vue-neo4j semantic-ui-vue

Import and register the plugins in main.js:

import Vue from 'vue'
import VueNeo4j from 'vue-neo4j'
import SuiVue from 'semantic-ui-vue'
import App from './App.vue'
import router from './router'
import 'semantic-ui-css/semantic.min.css'// Tell Vue to use these plugins
Vue.use(VueNeo4j)
Vue.use(SuiVue)
new Vue({
router,
render: h => h(App),
}).$mount('#app')

Setup each of the individual routes in Vue-Router:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from './routes/Home'
import Config from './routes/Config'
import Import from './routes/Import'
import Preview from './routes/Preview'
import Delete from './routes/Delete'
import Export from './routes/Export'
Vue.use(VueRouter)export default new VueRouter({
routes: [
{ name: 'config', path: '/config', component: Config, },
{ name: 'import', path: '/import', component: Import, },
{ name: 'preview', path: '/preview', component: Preview, },
{ name: 'delete', path: '/delete', component: Delete, },
{ name: 'export', path: '/export', component: Export, },
// Redirect to Home
{ name: 'home', path: '/', component: Home, },
{ path: '*', redirect: '/', },
]
})

Create a vue mixin to supply methods to run queries against the database:

export default {
data: () => ({
loading: false, error: false, result: false, tab: 0,
}),
computed: {
cypher: () => 'MATCH (n) RETURN count(n) AS count',
params: () => {},
},
methods: {
runQuery() {
this.loading = true
this.error = false
this.$neo4j.run(this.cypher, this.params)
.then(res => this.result = res)
.catch(e => this.error = e)
.finally(() => {
this.tab = 1
this.loading = false
})
},
},
}

Then, piece everything together in a route component, using the runQuery method from the mixin to execute the computed cypher variable.

import CypherComponent from './CypherComponent'export default {
name: 'config',
mixins: [ CypherComponent, ],
// ...
computed: {
cypher() {
let params = []
const keys = ['handleVocabUris', 'handleMultival',
'handleRDFTypes']
keys.map(key => {
if ( this[ key ] !== undefined && this[key] !== '' )
params.push(`${key}: '${this[key]}'`)
})
if ( params.length ) {
params = `{\n\t${params.join(',\n\t')}\n}`
}
return `CALL n10s.graphconfig.init(${params})`
},
},
}

You can view the entire source code for the n10s app on Github at https://github.com/neo4j-apps/n10s

A confession

As a Developer Experience Engineer at Neo4j, it is my job to build apps like these. But I’m also here to make your life easier as a developer. If there is anything that I can do to improve your experience while developing Graph Apps; whether that be improved documentation, better tutorials and guides or frameworks and reusable components for your favourite language/framework, I’m all ears.

Feel free to reach out to me on Twitter or post a message on the Neo4j Community site.

Neo4j Developer Blog

Developer Content around Graph Databases, Neo4j, Cypher…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store