neo4j.cr

Join the chat at https://gitter.im/jgaskins/neo4j.cr

Crystal implementation of a Neo4j driver using the Bolt protocol.

Installation

Add this to your application's shard.yml:

dependencies:
  neo4j:
    github: jgaskins/neo4j.cr

Usage

First you need to set up a connection:

require "neo4j"

# The `ssl` option defaults to `true` so you don't accidentally send the
# password to your production DB in cleartext.
connection = Neo4j::Bolt::Connection.new(
  "bolt://neo4j:password@localhost:7687",
  ssl: false,
)

The connection has two public methods:

execute(query : String, params = ({} of String => Neo4j::Type))

Executes the given Cypher query. Takes a hash of params for sanitization and query caching.

result = connection.execute("
  MATCH (order:Order)-[:ORDERS]->(product:Product)
  RETURN order, collect(product)
  LIMIT 10
")

This method returns a Neo4j::Result. You can iterate over it with Enumerable methods. Each iteration of the block will return an array of the values passed to the query's RETURN clause:

result = connection.execute(<<-CYPHER, { "email" => "[email protected]" })
  MATCH (self:User)<-[:SENT_TO]-(message:Message)-[:SENT_BY]->(author:User)
  WHERE self.email == $email
  RETURN author, message
CYPHER

result.map do |(author, message)| # Using () to destructure the array into block args
  do_something(
    author: author.as(Neo4j::Node),
    message: message.as(Neo4j::Node),
  )
end

Note that we cast the values returned from the query into Neo4j::Node. Each value returned from a query can be any Neo4j data type and cannot be known at compile time, so we have to cast the values into the types we know them to be — in this case, we are returning nodes.

transaction(&block)

Executes the block within the context of a Neo4j transaction. At the end of the block, the transaction is committed. If an exception is raised, the transaction will be rolled back and the connection will be reset to a clean state.

Example:

connection.transaction do
  query = <<-CYPHER
    CREATE (user:User {
      uuid: $uuid,
      name: $name,
      email: $email,
    })
  CYPHER

  connection.execute(query, params.merge({ "uuid" => UUID.random.to_s }))
end

reset

Resets a connection to a clean state. A connection will automatically call reset if an exception is raised within a transaction, so you shouldn't have to call this explicitly, but it's provided just in case.

Neo4j::Result

The Result object itself is an Enumerable. Calling Result#each will iterate over the data for you.

Neo4j::Node

These have a 1:1 mapping to nodes in your graph.

Neo4j::Relationship

Neo4j::Type

Represents any data type that can be stored in a Neo4j database and communicated via the Bolt protocol. It's a shorthand for this union type:

Nil |
Bool |
String |
Int8 |
Int16 |
Int32 |
Int64 |
Float64 |
Array(Neo4j::Type) |
Hash(String, Neo4j::Type) |
Neo4j::Node |
Neo4j::Relationship |
Neo4j::UnboundRelationship |
Neo4j::Path |
Neo4j::Success |
Neo4j::Failure |
Neo4j::Ignored

Mapping to Domain Objects

Similar to JSON.mapping in the Crystal standard library, you can map nodes and relationships to domain objects. For example:

require "uuid"

class User
  Neo4j.map_node(
    uuid: UUID,
    email: String,
    name: String
    registered_at: Time,
  )
end

class Product
  Neo4j.map_node(
    uuid: UUID,
    name: String,
    description: String,
    price: Int32,
    created_at: Time,
  )
end

class CartItem
  Neo4j.map_relationship(
    quantity: Int32,
    price: Int32,
  )
end

With these in place, you can build them from your nodes and relationships:

result = connection.execute(<<-CYPHER, { "uuid" => params["uuid"] })
  MATCH (product:Product)-[cart_item:IN_CART]->(user:User { uuid: $uuid })
  RETURN product, cart_item
CYPHER

cart = Cart.new(result.map { |(product, cart_item)|
  {
    product: Product.new(product.as(Neo4j::Node)),
    cart_item: CartItem.new(cart_item.as(Neo4j::Relationship)),
  }
})

Caveats/Limitations

Future development

Acknowledgements/Credits

This implementation is heavily based on @benoist's implementation of MessagePack. I had never built a binary protocol parser in a statically typed language before, so it really helped jump-start the development of this project.

Contributing

  1. Fork it ( https://github.com/jgaskins/neo4j.cr/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors