module neo4j
import curl_json
+import error
# Handles Neo4j server start and stop command
#
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
end
# 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]
- var batch = new NeoBatch(self)
- for obj in res.as(JsonArray) do
- var node = new NeoNode.from_json(self, obj.as(JsonObject))
- batch.load_node(node)
+ for json in res.as(JsonArray) do
+ var obj = json.as(JsonObject)
+ var node = load_node(obj["self"].to_s)
+ node.internal_properties = obj["data"].as(JsonObject)
+ nodes.add node
+ end
+ return nodes
+ end
+
+ # 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
+
+ # 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)
+ var node = load_node(obj["self"].to_s)
+ node.internal_properties = obj["data"].as(JsonObject)
nodes.add node
end
- batch.execute
return nodes
end
# 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`
# 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
# 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.
# 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
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.
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
# 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
# Load a node in batch mode also load labels, data and edges
fun load_node(node: NeoNode) do
- load_node_data(node)
- load_node_labels(node)
- load_node_out_edges(node)
- end
-
- # Load data into node
- private fun load_node_data(node: NeoNode) do
var job = new_job(node)
job.action = load_node_data_action
job.method = "GET"
else
job.to = "\{{node.batch_id.to_s}\}"
end
- end
-
- # Load labels into node
- private fun load_node_labels(node: NeoNode) do
- var job = new_job(node)
+ job = new_job(node)
job.action = load_node_labels_action
job.method = "GET"
if node.id != null then
end
end
- # Load out edges into node
- private fun load_node_out_edges(node: NeoNode) do
+ # Load in and out edges into node
+ fun load_node_edges(node: NeoNode) do
var job = new_job(node)
+ job.action = load_node_in_edges_action
+ job.method = "GET"
+ if node.id != null then
+ job.to = "/node/{node.id.to_s}/relationships/in"
+ else
+ job.to = "\{{node.batch_id.to_s}\}/relationships/in"
+ end
+ job = new_job(node)
job.action = load_node_out_edges_action
job.method = "GET"
if node.id != null then
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
job.to = "\{{node.batch_id.to_s}\}/labels"
job.body = new JsonArray.from(node.labels)
# add edges
- save_edges(node.out_edges)
+ #save_edges(node.out_edges)
end
# Create multiple nodes
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
+ fun execute: List[NeoError] do
var request = new JsonPOST(client.batch_url, client.curl)
# 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 labels = new Array[String]
for l in res["body"].as(JsonArray) do labels.add l.to_s
node.internal_labels = labels
+ else if job.action == load_node_in_edges_action then
+ var node = job.entity.as(NeoNode)
+ var edges = res["body"].as(JsonArray)
+ node.internal_in_edges = new List[NeoEdge]
+ for edge in edges do
+ node.internal_in_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
+ end
else if job.action == load_node_out_edges_action then
var node = job.entity.as(NeoNode)
var edges = res["body"].as(JsonArray)
node.internal_out_edges = new List[NeoEdge]
for edge in edges do
- node.internal_out_edges.add new NeoEdge.from_json(client, edge.as(JsonObject))
+ node.internal_out_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
end
end
end
private fun create_edge_action: Int do return 2
private fun load_node_data_action: Int do return 3
private fun load_node_labels_action: Int do return 4
- private fun load_node_out_edges_action: Int do return 5
+ private fun load_node_in_edges_action: Int do return 5
+ private fun load_node_out_edges_action: Int do return 6
end
# A job that can be executed in a `NeoBatch`
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