#
# In order to connect to Neo4j you need a connector:
#
-# # Create new Neo4j client
-# var client = new Neo4jClient("http://localhost:7474")
-# assert client.is_ok
+# # Create new Neo4j client
+# var client = new Neo4jClient("http://localhost:7474")
+# assert client.is_ok
#
# The fundamental units that form a graph are nodes and relationships.
#
# Nodes are used to represent entities stored in base:
#
-# # Create a disconnected node
-# var andres = new NeoNode
-# andres["name"] = "Andres"
-# # Connect the node to Neo4j
-# client.save_node(andres)
-# assert andres.is_linked
-# #
-# # Create a second node
-# var kate = new NeoNode
-# kate["name"] = "Kate"
-# client.save_node(kate)
-# assert kate.is_linked
+# # Create a disconnected node
+# var andres = new NeoNode
+# andres["name"] = "Andres"
+# # Connect the node to Neo4j
+# client.save_node(andres)
+# assert andres.is_linked
+#
+# # Create a second node
+# var kate = new NeoNode
+# kate["name"] = "Kate"
+# client.save_node(kate)
+# assert kate.is_linked
#
# Relationships between nodes are a key part of a graph database.
# They allow for finding related data. Just like nodes, relationships can have properties.
#
-# # Create a relationship
-# var loves = new NeoEdge(andres, "LOVES", kate)
-# client.save_edge(loves)
-# assert loves.is_linked
+# # Create a relationship
+# var loves = new NeoEdge(andres, "LOVES", kate)
+# client.save_edge(loves)
+# assert loves.is_linked
#
# Nodes can also be loaded fron Neo4j:
#
-# # Get a node from DB and explore edges
-# var url = andres.url.to_s
-# var from = client.load_node(url)
-# assert from["name"].to_s == "Andres"
-# var to = from.out_nodes("LOVES").first # follow the first LOVES relationship
-# assert to["name"].to_s == "Kate"
+# # Get a node from DB and explore edges
+# var url = andres.url.to_s
+# var from = client.load_node(url)
+# assert from["name"].to_s == "Andres"
+# var to = from.out_nodes("LOVES").first # follow the first LOVES relationship
+# assert to["name"].to_s == "Kate"
#
# For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html
module neo4j
import curl_json
+import error
# Handles Neo4j server start and stop command
#
# `Neo4jClient` is needed to communicate through the REST API
#
-# var client = new Neo4jClient("http://localhost:7474")
-# assert client.is_ok
+# var client = new Neo4jClient("http://localhost:7474")
+# assert client.is_ok
class Neo4jClient
# Neo4j REST services baseurl
init(base_url: String) do
self.base_url = base_url
var root = service_root
- if not root isa JsonObject then
- print "Neo4jClientError: cannot connect to server at {base_url}"
- abort
+ assert root isa JsonObject else
+ sys.stderr.write "Neo4jClientError: cannot connect to server at <{base_url}>.\n"
end
self.node_url = root["node"].to_s
self.batch_url = root["batch"].to_s
self.cypher_url = root["cypher"].to_s
end
- fun service_root: Jsonable do return get("{base_url}/db/data")
+ fun service_root: Jsonable do return get(base_url / "db/data")
# Is the connection with the Neo4j server ok?
fun is_ok: Bool do return service_root isa JsonObject
# Save the node in base
#
# var client = new Neo4jClient("http://localhost:7474")
- # #
+ #
# # Create a node
# var andres = new NeoNode
# andres["name"] = "Andres"
# From and to nodes will be created.
#
# var client = new Neo4jClient("http://localhost:7474")
- # #
+ #
# var andres = new NeoNode
# var kate = new NeoNode
# var edge = new NeoEdge(andres, "LOVES", kate)
# Retrieve all nodes with specified `lbl`
#
# var client = new Neo4jClient("http://localhost:7474")
- # #
+ #
# var andres = new NeoNode
# andres.labels.add_all(["Human", "Male"])
# client.save_node(andres)
# var kate = new NeoNode
# kate.labels.add_all(["Human", "Female"])
# client.save_node(kate)
- # #
+ #
# var nodes = client.nodes_with_label("Human")
# assert nodes.has(andres)
# assert nodes.has(kate)
fun nodes_with_label(lbl: String): Array[NeoNode] do
- var res = get("{base_url}/db/data/label/{lbl}/nodes")
+ var res = get(base_url / "db/data/label/{lbl.to_percent_encoding}/nodes")
var nodes = new Array[NeoNode]
for json in res.as(JsonArray) do
var obj = json.as(JsonObject)
# Retrieve nodes belonging to all the specified `labels`.
#
# var client = new Neo4jClient("http://localhost:7474")
- # #
+ #
# var andres = new NeoNode
# andres.labels.add_all(["Human", "Male"])
# client.save_node(andres)
# var kate = new NeoNode
# kate.labels.add_all(["Human", "Female"])
# client.save_node(kate)
- # #
+ #
# var nodes = client.nodes_with_labels(["Human", "Male"])
# assert nodes.has(andres)
# assert not nodes.has(kate)
fun nodes_with_labels(labels: Array[String]): Array[NeoNode] do
assert not labels.is_empty
- var res = cypher(new CypherQuery.from_string("MATCH (n:{labels.join(":")}) RETURN n"))
+
+ # Build the query.
+ var buffer = new RopeBuffer
+ buffer.append "match n where \{label_0\} in labels(n)"
+ for i in [1..labels.length[ do
+ buffer.append " and \{label_{i}\} in labels(n)"
+ end
+ buffer.append " return n"
+ var query = new CypherQuery.from_string(buffer.write_to_string)
+ for i in [0..labels.length[ do
+ query.params["label_{i}"] = labels[i]
+ end
+
+ # Retrieve the answer.
+ var res = cypher(query)
var nodes = new Array[NeoNode]
for json in res.as(JsonObject)["data"].as(JsonArray) do
var obj = json.as(JsonArray).first.as(JsonObject)
# Perform a `CypherQuery`
# see: CypherQuery
fun cypher(query: CypherQuery): Jsonable do
- return post("{cypher_url}", query.to_json)
+ return post("{cypher_url}", query.to_rest)
end
# GET JSON data from `url`
fun get(url: String): Jsonable do
- var request = new JsonGET(url, curl)
+ var request = new JsonGET(url)
var response = request.execute
return parse_response(response)
end
# POST `params` to `url`
fun post(url: String, params: Jsonable): Jsonable do
- var request = new JsonPOST(url, curl)
+ var request = new JsonPOST(url)
request.data = params
var response = request.execute
return parse_response(response)
# PUT `params` at `url`
fun put(url: String, params: Jsonable): Jsonable do
- var request = new JsonPUT(url, curl)
+ var request = new JsonPUT(url)
request.data = params
var response = request.execute
return parse_response(response)
# DELETE `url`
fun delete(url: String): Jsonable do
- var request = new JsonDELETE(url, curl)
+ var request = new JsonDELETE(url)
var response = request.execute
return parse_response(response)
end
# Parse the cURL `response` as a JSON string
private fun parse_response(response: CurlResponse): Jsonable do
if response isa CurlResponseSuccess then
- if response.body_str.is_empty then
+ var str = response.body_str
+ if str.is_empty then return new JsonObject
+ var res = str.parse_json
+ if res isa JsonParseError then
+ var e = new NeoError(res.to_s, "JsonParseError")
+ e.cause = res
+ return e
+ end
+ if res == null then
+ # empty response wrap it in empty object
return new JsonObject
- else
- var str = response.body_str
- var res = str.to_jsonable
- if res == null then
- # empty response wrap it in empty object
- return new JsonObject
- else if res isa JsonObject and res.has_key("exception") then
- var error = "Neo4jError::{res["exception"] or else "null"}"
- var msg = ""
- if res.has_key("message") then
- msg = res["message"].to_s
- end
- return new JsonError(error, msg.to_json)
- else
- return res
+ else if res isa JsonObject and res.has_key("exception") then
+ var error = "Neo4jError::{res["exception"] or else "null"}"
+ var msg = ""
+ if res.has_key("message") then
+ msg = res["message"].to_s
end
+ return new NeoError(msg, error)
+ else
+ return res
end
else if response isa CurlResponseFailed then
- return new JsonError("Curl error", "{response.error_msg} ({response.error_code})")
+ return new NeoError("{response.error_msg} ({response.error_code})", "CurlError")
else
- return new JsonError("Curl error", "Unexpected response '{response}'")
+ return new NeoError("Unexpected response \"{response}\".", "CurlError")
end
end
end
#
# Example:
#
-# var client = new Neo4jClient("http://localhost:7474")
-# var query = new CypherQuery
-# query.nmatch("(n)-[r:LOVES]->(m)")
-# query.nwhere("n.name=\"Andres\"")
-# query.nreturn("m.name")
-# var res = client.cypher(query).as(JsonObject)
-# assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
+# var client = new Neo4jClient("http://localhost:7474")
+# var query = new CypherQuery
+# query.nmatch("(n)-[r:LOVES]->(m)")
+# query.nwhere("n.name=\"Andres\"")
+# query.nreturn("m.name")
+# var res = client.cypher(query).as(JsonObject)
+# assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
#
# For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
class CypherQuery
# Query string to perform
- private var query: String
+ private var query: String = ""
# `params` to embed in the query like in prepared statements
var params = new JsonObject
- init do
- self.query = ""
- end
-
# init the query from a query string
init from_string(query: String) do
self.query = query
return self
end
- # Translate the query to JSON
- fun to_json: JsonObject do
+ # Translate the query to the body of a corresponding Neo4j REST request.
+ fun to_rest: JsonObject do
var obj = new JsonObject
obj["query"] = query
if not params.is_empty then
return obj
end
- redef fun to_s do return to_json.to_s
+ redef fun to_s do return to_rest.to_s
end
# The fundamental units that form a graph are nodes and relationships.
#
# If the entity is initialized unlinked from neo4j:
#
-# # Create a disconnected node
-# var andres = new NeoNode
-# andres["name"] = "Andres"
-# # At this point, the node is not linked
-# assert not andres.is_linked
+# # Create a disconnected node
+# var andres = new NeoNode
+# andres["name"] = "Andres"
+# # At this point, the node is not linked
+# assert not andres.is_linked
#
# Then we can link the entity to the base:
#
# assert node["name"] == "Andres" # loaded lazily from base
abstract class NeoEntity
# Neo4j client connector
- private var neo: Neo4jClient
+ private var neo: Neo4jClient is noinit
# Entity unique URL in Neo4j REST API
- var url: nullable String
+ var url: nullable String = null
# Temp id used in batch mode to update the entity
private var batch_id: nullable Int = null
# Load the entity from base
- private init from_neo(neo: Neo4jClient, url: String) do
+ private init from_neo(neo: Neo4jClient, url: String) is nosuper do
self.neo = neo
self.url = url
end
# Init entity from JSON representation
- private init from_json(neo: Neo4jClient, obj: JsonObject) do
+ private init from_json(neo: Neo4jClient, obj: JsonObject) is nosuper do
self.neo = neo
self.url = obj["self"].to_s
self.internal_properties = obj["data"].as(JsonObject)
private var internal_properties: nullable JsonObject = null
private fun load_properties: JsonObject do
- var obj = neo.get("{url.to_s}/properties").as(JsonObject)
+ var obj = neo.get(url.to_s / "properties").as(JsonObject)
internal_properties = obj
return obj
end
# Is the property `key` set?
fun has_key(key: String): Bool do return properties.has_key(key)
-
- # Translate `self` to JSON
- fun to_json: JsonObject do return properties
end
# Nodes are used to represent entities stored in base.
#
# Creating new nodes:
#
-# var client = new Neo4jClient("http://localhost:7474")
-# #
-# var andres = new NeoNode
-# andres.labels.add "Person"
-# andres["name"] = "Andres"
-# andres["age"] = 22
-# client.save_node(andres)
-# assert andres.is_linked
+# var client = new Neo4jClient("http://localhost:7474")
+#
+# var andres = new NeoNode
+# andres.labels.add "Person"
+# andres["name"] = "Andres"
+# andres["age"] = 22
+# client.save_node(andres)
+# assert andres.is_linked
#
# Get nodes from Neo4j:
#
-# var url = andres.url.to_s
-# var node = client.load_node(url)
-# assert node["name"] == "Andres"
-# assert node["age"].to_s.to_i == 22
+# var url = andres.url.to_s
+# var node = client.load_node(url)
+# assert node["name"] == "Andres"
+# assert node["age"].to_s.to_i == 22
class NeoNode
super NeoEntity
var tpl = new FlatBuffer
tpl.append "\{"
tpl.append "labels: [{labels.join(", ")}],"
- tpl.append "data: {to_json}"
+ tpl.append "data: {properties.to_json}"
tpl.append "\}"
return tpl.write_to_string
end
private fun load_labels: Array[String] do
var labels = new Array[String]
- var res = neo.get("{url.to_s}/labels")
+ var res = neo.get(url.to_s / "labels")
if res isa JsonArray then
for val in res do labels.add val.to_s
end
private fun load_in_edges: List[NeoEdge] do
var edges = new List[NeoEdge]
- var res = neo.get("{url.to_s}/relationships/in").as(JsonArray)
+ var res = neo.get(url.to_s / "relationships/in").as(JsonArray)
for obj in res do
edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
end
private fun load_out_edges: List[NeoEdge] do
var edges = new List[NeoEdge]
- var res = neo.get("{url.to_s}/relationships/out")
+ var res = neo.get(url.to_s / "relationships/out")
for obj in res.as(JsonArray) do
edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
end
#
# Create a relationship:
#
-# var client = new Neo4jClient("http://localhost:7474")
-# # Create nodes
-# var andres = new NeoNode
-# andres["name"] = "Andres"
-# var kate = new NeoNode
-# kate["name"] = "Kate"
-# # Create a relationship of type `LOVES`
-# var loves = new NeoEdge(andres, "LOVES", kate)
-# client.save_edge(loves)
-# assert loves.is_linked
+# var client = new Neo4jClient("http://localhost:7474")
+# # Create nodes
+# var andres = new NeoNode
+# andres["name"] = "Andres"
+# var kate = new NeoNode
+# kate["name"] = "Kate"
+# # Create a relationship of type `LOVES`
+# var loves = new NeoEdge(andres, "LOVES", kate)
+# client.save_edge(loves)
+# assert loves.is_linked
#
# Get an edge from DB:
#
-# var url = loves.url.to_s
-# var edge = client.load_edge(url)
-# assert edge.from["name"].to_s == "Andres"
-# assert edge.to["name"].to_s == "Kate"
+# var url = loves.url.to_s
+# var edge = client.load_edge(url)
+# assert edge.from["name"].to_s == "Andres"
+# assert edge.to["name"].to_s == "Kate"
class NeoEdge
super NeoEntity
# Get edge type
fun rel_type: nullable String do return internal_type
- redef fun to_json do
+ # Get the JSON body of a REST request that create the relationship.
+ private fun to_rest: JsonObject do
var obj = new JsonObject
if to.is_linked then
obj["to"] = to.url
#
# Example:
#
-# var client = new Neo4jClient("http://localhost:7474")
-# #
-# var node1 = new NeoNode
-# var node2 = new NeoNode
-# var edge = new NeoEdge(node1, "TO", node2)
-# #
-# var batch = new NeoBatch(client)
-# batch.save_node(node1)
-# batch.save_node(node2)
-# batch.save_edge(edge)
-# batch.execute
-# #
-# assert node1.is_linked
-# assert node2.is_linked
-# assert edge.is_linked
+# var client = new Neo4jClient("http://localhost:7474")
+#
+# var node1 = new NeoNode
+# var node2 = new NeoNode
+# var edge = new NeoEdge(node1, "TO", node2)
+#
+# var batch = new NeoBatch(client)
+# batch.save_node(node1)
+# batch.save_node(node2)
+# batch.save_edge(edge)
+# batch.execute
+#
+# assert node1.is_linked
+# assert node2.is_linked
+# assert edge.is_linked
class NeoBatch
# Neo4j client connector
end
end
+ # Create a `NeoNode` or a `NeoEdge` in batch mode.
+ fun save_entity(nentity: NeoEntity) do
+ if nentity isa NeoNode then
+ save_node(nentity)
+ else if nentity isa NeoEdge then
+ save_edge(nentity)
+ else abort
+ end
+
# Create a node in batch mode also create labels and edges
fun save_node(node: NeoNode) do
if node.id != null or node.batch_id != null then return
else
job.to = "\{{edge.from.batch_id.to_s}\}/relationships"
end
- job.body = edge.to_json
+ job.body = edge.to_rest
end
# Create multiple edges
fun save_edges(edges: Collection[NeoEdge]) do for edge in edges do save_edge(edge)
# Execute the batch and update local nodes
- fun execute: List[JsonError] do
- var request = new JsonPOST(client.batch_url, client.curl)
+ fun execute: List[NeoError] do
+ var request = new JsonPOST(client.batch_url)
# request.headers["X-Stream"] = "true"
var json_jobs = new JsonArray
- for job in jobs.values do json_jobs.add job.to_json
+ for job in jobs.values do json_jobs.add job.to_rest
request.data = json_jobs
var response = request.execute
var res = client.parse_response(response)
end
# Associate data from response in original nodes and edges
- private fun finalize_batch(response: Jsonable): List[JsonError] do
- var errors = new List[JsonError]
+ private fun finalize_batch(response: Jsonable): List[NeoError] do
+ var errors = new List[NeoError]
if not response isa JsonArray then
- errors.add(new JsonError("Neo4jError", "Unexpected batch response format"))
+ errors.add(new NeoError("Unexpected batch response format.", "Neo4jError"))
return errors
end
# print " {res.length} jobs executed"
for res in response do
if not res isa JsonObject then
- errors.add(new JsonError("Neo4jError", "Unexpected job format in batch response"))
+ errors.add(new NeoError("Unexpected job format in batch response.", "Neo4jError"))
continue
end
var id = res["id"].as(Int)
var body: nullable Jsonable = null
# JSON formated job
- fun to_json: JsonObject do
+ fun to_rest: JsonObject do
var job = new JsonObject
job["id"] = id
job["method"] = method
return job
end
end
-