1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Neo4j connector through its JSON REST API using curl.
17 # For ease of use and testing this module provide a wrapper to the `neo4j` command:
19 # # Start the Neo4j server
20 # var srv = new Neo4jServer
21 # assert srv.start_quiet
23 # In order to connect to Neo4j you need a connector:
25 # # Create new Neo4j client
26 # var client = new Neo4jClient("http://localhost:7474")
29 # The fundamental units that form a graph are nodes and relationships.
31 # Nodes are used to represent entities stored in base:
33 # # Create a disconnected node
34 # var andres = new NeoNode
35 # andres["name"] = "Andres"
36 # # Connect the node to Neo4j
37 # client.save_node(andres)
38 # assert andres.is_linked
40 # # Create a second node
41 # var kate = new NeoNode
42 # kate["name"] = "Kate"
43 # client.save_node(kate)
44 # assert kate.is_linked
46 # Relationships between nodes are a key part of a graph database.
47 # They allow for finding related data. Just like nodes, relationships can have properties.
49 # # Create a relationship
50 # var loves = new NeoEdge(andres, "LOVES", kate)
51 # client.save_edge(loves)
52 # assert loves.is_linked
54 # Nodes can also be loaded fron Neo4j:
56 # # Get a node from DB and explore edges
57 # var url = andres.url.to_s
58 # var from = client.load_node(url)
59 # assert from["name"].to_s == "Andres"
60 # var to = from.out_nodes("LOVES").first # follow the first LOVES relationship
61 # assert to["name"].to_s == "Kate"
63 # For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html
68 # Handles Neo4j server start and stop command
70 # `neo4j` binary must be in `PATH` in order to work
73 # Start the local Neo4j server instance
75 sys
.system
("neo4j start console")
79 # Like `start` but redirect the console output to `/dev/null`
80 fun start_quiet
: Bool do
81 sys
.system
("neo4j start console > /dev/null")
85 # Stop the local Neo4j server instance
87 sys
.system
("neo4j stop")
91 # Like `stop` but redirect the console output to `/dev/null`
92 fun stop_quiet
: Bool do
93 sys
.system
("neo4j stop > /dev/null")
98 # `Neo4jClient` is needed to communicate through the REST API
100 # var client = new Neo4jClient("http://localhost:7474")
101 # assert client.is_ok
104 # Neo4j REST services baseurl
106 # REST service to get node data
107 private var node_url
: String
108 # REST service to batch
109 private var batch_url
: String
110 # REST service to send cypher requests
111 private var cypher_url
: String
113 private var curl
= new Curl
115 init(base_url
: String) do
116 self.base_url
= base_url
117 var root
= service_root
118 assert root
isa JsonObject else
119 sys
.stderr
.write
"Neo4jClientError: cannot connect to server at <{base_url}>.\n"
121 self.node_url
= root
["node"].to_s
122 self.batch_url
= root
["batch"].to_s
123 self.cypher_url
= root
["cypher"].to_s
126 fun service_root
: Jsonable do return get
("{base_url}/db/data")
128 # Is the connection with the Neo4j server ok?
129 fun is_ok
: Bool do return service_root
isa JsonObject
133 cypher
(new CypherQuery.from_string
("MATCH (n) OPTIONAL MATCH n-[r]-() DELETE r, n"))
137 var errors
= new Array[String]
139 # Nodes view stored locally
140 private var local_nodes
= new HashMap[String, nullable NeoNode]
142 # Save the node in base
144 # var client = new Neo4jClient("http://localhost:7474")
147 # var andres = new NeoNode
148 # andres["name"] = "Andres"
149 # client.save_node(andres)
150 # assert andres.is_linked
152 # Once linked, nodes cannot be created twice:
154 # var oldurl = andres.url
155 # client.save_node(andres) # do nothing
156 # assert andres.url == oldurl
157 fun save_node
(node
: NeoNode): Bool do
158 if node
.is_linked
then return true
160 var batch
= new NeoBatch(self)
161 batch
.save_node
(node
)
162 # batch.create_edges(node.out_edges)
163 var errors
= batch
.execute
164 if not errors
.is_empty
then
165 errors
.add_all errors
168 local_nodes
[node
.url
.to_s
] = node
172 # Load a node from base
173 # Data, labels and edges will be loaded lazily.
174 fun load_node
(url
: String): NeoNode do
175 if local_nodes
.has_key
(url
) then
176 var node
= local_nodes
[url
]
177 if node
!= null then return node
179 var node
= new NeoNode.from_neo
(self, url
)
180 local_nodes
[url
] = node
184 # Remove the entity from base
185 fun delete_node
(node
: NeoNode): Bool do
186 if not node
.is_linked
then return false
187 var url
= node
.url
.to_s
189 local_nodes
[url
] = null
194 # Edges view stored locally
195 private var local_edges
= new HashMap[String, nullable NeoEdge]
197 # Save the edge in base
198 # From and to nodes will be created.
200 # var client = new Neo4jClient("http://localhost:7474")
202 # var andres = new NeoNode
203 # var kate = new NeoNode
204 # var edge = new NeoEdge(andres, "LOVES", kate)
205 # client.save_edge(edge)
206 # assert andres.is_linked
207 # assert kate.is_linked
208 # assert edge.is_linked
209 fun save_edge
(edge
: NeoEdge): Bool do
210 if edge
.is_linked
then return true
212 edge
.from
.out_edges
.add edge
213 edge
.to
.in_edges
.add edge
214 var batch
= new NeoBatch(self)
215 batch
.save_edge
(edge
)
216 var errors
= batch
.execute
217 if not errors
.is_empty
then
218 errors
.add_all errors
221 local_edges
[edge
.url
.to_s
] = edge
225 # Load a edge from base
226 # Data will be loaded lazily.
227 fun load_edge
(url
: String): NeoEdge do
228 if local_edges
.has_key
(url
) then
229 var node
= local_edges
[url
]
230 if node
!= null then return node
232 var edge
= new NeoEdge.from_neo
(self, url
)
233 local_edges
[url
] = edge
237 # Remove the edge from base
238 fun delete_edge
(edge
: NeoEdge): Bool do
239 if not edge
.is_linked
then return false
240 var url
= edge
.url
.to_s
242 local_edges
[url
] = null
247 # Retrieve all nodes with specified `lbl`
249 # var client = new Neo4jClient("http://localhost:7474")
251 # var andres = new NeoNode
252 # andres.labels.add_all(["Human", "Male"])
253 # client.save_node(andres)
254 # var kate = new NeoNode
255 # kate.labels.add_all(["Human", "Female"])
256 # client.save_node(kate)
258 # var nodes = client.nodes_with_label("Human")
259 # assert nodes.has(andres)
260 # assert nodes.has(kate)
261 fun nodes_with_label
(lbl
: String): Array[NeoNode] do
262 var res
= get
("{base_url}/db/data/label/{lbl}/nodes")
263 var nodes
= new Array[NeoNode]
264 for json
in res
.as(JsonArray) do
265 var obj
= json
.as(JsonObject)
266 var node
= load_node
(obj
["self"].to_s
)
267 node
.internal_properties
= obj
["data"].as(JsonObject)
273 # Retrieve nodes belonging to all the specified `labels`.
275 # var client = new Neo4jClient("http://localhost:7474")
277 # var andres = new NeoNode
278 # andres.labels.add_all(["Human", "Male"])
279 # client.save_node(andres)
280 # var kate = new NeoNode
281 # kate.labels.add_all(["Human", "Female"])
282 # client.save_node(kate)
284 # var nodes = client.nodes_with_labels(["Human", "Male"])
285 # assert nodes.has(andres)
286 # assert not nodes.has(kate)
287 fun nodes_with_labels
(labels
: Array[String]): Array[NeoNode] do
288 assert not labels
.is_empty
289 var res
= cypher
(new CypherQuery.from_string
("MATCH (n:{labels.join(":")}) RETURN n"))
290 var nodes
= new Array[NeoNode]
291 for json
in res
.as(JsonObject)["data"].as(JsonArray) do
292 var obj
= json
.as(JsonArray).first
.as(JsonObject)
293 var node
= load_node
(obj
["self"].to_s
)
294 node
.internal_properties
= obj
["data"].as(JsonObject)
300 # Perform a `CypherQuery`
302 fun cypher
(query
: CypherQuery): Jsonable do
303 return post
("{cypher_url}", query
.to_rest
)
306 # GET JSON data from `url`
307 fun get
(url
: String): Jsonable do
308 var request
= new JsonGET(url
, curl
)
309 var response
= request
.execute
310 return parse_response
(response
)
313 # POST `params` to `url`
314 fun post
(url
: String, params
: Jsonable): Jsonable do
315 var request
= new JsonPOST(url
, curl
)
316 request
.data
= params
317 var response
= request
.execute
318 return parse_response
(response
)
321 # PUT `params` at `url`
322 fun put
(url
: String, params
: Jsonable): Jsonable do
323 var request
= new JsonPUT(url
, curl
)
324 request
.data
= params
325 var response
= request
.execute
326 return parse_response
(response
)
330 fun delete
(url
: String): Jsonable do
331 var request
= new JsonDELETE(url
, curl
)
332 var response
= request
.execute
333 return parse_response
(response
)
336 # Parse the cURL `response` as a JSON string
337 private fun parse_response
(response
: CurlResponse): Jsonable do
338 if response
isa CurlResponseSuccess then
339 if response
.body_str
.is_empty
then
340 return new JsonObject
342 var str
= response
.body_str
343 var res
= str
.to_jsonable
345 # empty response wrap it in empty object
346 return new JsonObject
347 else if res
isa JsonObject and res
.has_key
("exception") then
348 var error
= "Neo4jError::{res["exception"] or else "null"}"
350 if res
.has_key
("message") then
351 msg
= res
["message"].to_s
353 return new JsonError(error
, msg
.to_json
)
358 else if response
isa CurlResponseFailed then
359 return new JsonError("Curl error", "{response.error_msg} ({response.error_code})")
361 return new JsonError("Curl error", "Unexpected response '{response}'")
366 # A Cypher query for Neo4j REST API
368 # The Neo4j REST API allows querying with Cypher.
369 # The results are returned as a list of string headers (columns), and a data part,
370 # consisting of a list of all rows, every row consisting of a list of REST representations
371 # of the field value - Node, Relationship, Path or any simple value like String.
375 # var client = new Neo4jClient("http://localhost:7474")
376 # var query = new CypherQuery
377 # query.nmatch("(n)-[r:LOVES]->(m)")
378 # query.nwhere("n.name=\"Andres\"")
379 # query.nreturn("m.name")
380 # var res = client.cypher(query).as(JsonObject)
381 # assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
383 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
385 # Query string to perform
386 private var query
: String = ""
388 # `params` to embed in the query like in prepared statements
389 var params
= new JsonObject
393 # init the query from a query string
394 init from_string
(query
: String) do
398 # init the query with parameters
399 init with_params
(params
: JsonObject) do
403 # Add a `CREATE` statement to the query
404 fun ncreate
(query
: String): CypherQuery do
405 self.query
= "{self.query}CREATE {query} "
409 # Add a `START` statement to the query
410 fun nstart
(query
: String): CypherQuery do
411 self.query
= "{self.query}START {query} "
415 # Add a `MATCH` statement to the query
416 fun nmatch
(query
: String): CypherQuery do
417 self.query
= "{self.query}MATCH {query} "
421 # Add a `WHERE` statement to the query
422 fun nwhere
(query
: String): CypherQuery do
423 self.query
= "{self.query}WHERE {query} "
427 # Add a `AND` statement to the query
428 fun nand
(query
: String): CypherQuery do
429 self.query
= "{self.query}AND {query} "
433 # Add a `RETURN` statement to the query
434 fun nreturn
(query
: String): CypherQuery do
435 self.query
= "{self.query}RETURN {query} "
439 # Translate the query to the body of a corresponding Neo4j REST request.
440 fun to_rest
: JsonObject do
441 var obj
= new JsonObject
443 if not params
.is_empty
then
444 obj
["params"] = params
449 redef fun to_s
do return to_rest
.to_s
452 # The fundamental units that form a graph are nodes and relationships.
454 # Entities can have two states:
456 # * linked: the NeoEntity references an existing node or edge in Neo4j
457 # * unlinked: the NeoEntity is not yet created in Neo4j
459 # If the entity is initialized unlinked from neo4j:
461 # # Create a disconnected node
462 # var andres = new NeoNode
463 # andres["name"] = "Andres"
464 # # At this point, the node is not linked
465 # assert not andres.is_linked
467 # Then we can link the entity to the base:
470 # var client = new Neo4jClient("http://localhost:7474")
471 # client.save_node(andres)
472 # # The node is now linked
473 # assert andres.is_linked
475 # Entities can also be loaded from Neo4j:
477 # # Get a node from Neo4j
478 # var url = andres.url.to_s
479 # var node = client.load_node(url)
480 # assert node.is_linked
482 # When working in connected mode, all reading operations are executed lazily on the base:
484 # # Get the node `name` property
485 # assert node["name"] == "Andres" # loaded lazily from base
486 abstract class NeoEntity
487 # Neo4j client connector
488 private var neo
: Neo4jClient is noinit
490 # Entity unique URL in Neo4j REST API
491 var url
: nullable String = null
493 # Temp id used in batch mode to update the entity
494 private var batch_id
: nullable Int = null
496 # Load the entity from base
497 private init from_neo
(neo
: Neo4jClient, url
: String) do
502 # Init entity from JSON representation
503 private init from_json
(neo
: Neo4jClient, obj
: JsonObject) do
505 self.url
= obj
["self"].to_s
506 self.internal_properties
= obj
["data"].as(JsonObject)
509 # Create a empty (and not-connected) entity
511 self.internal_properties
= new JsonObject
514 # Is the entity linked to a Neo4j database?
515 fun is_linked
: Bool do return url
!= null
517 # In Neo4j, both nodes and relationships can contain properties.
518 # Properties are key-value pairs where the key is a string.
519 # Property values are JSON formatted.
521 # Properties are loaded lazily
522 fun properties
: JsonObject do return internal_properties
or else load_properties
524 private var internal_properties
: nullable JsonObject = null
526 private fun load_properties
: JsonObject do
527 var obj
= neo
.get
("{url.to_s}/properties").as(JsonObject)
528 internal_properties
= obj
532 # Get the entity `id` if connected to base
533 fun id
: nullable Int do
534 if url
== null then return null
535 return url
.split
("/").last
.to_i
538 # Get the entity property at `key`
539 fun [](key
: String): nullable Jsonable do
540 if not properties
.has_key
(key
) then return null
541 return properties
[key
]
544 # Set the entity property `value` at `key`
545 fun []=(key
: String, value
: nullable Jsonable) do properties
[key
] = value
547 # Is the property `key` set?
548 fun has_key
(key
: String): Bool do return properties
.has_key
(key
)
551 # Nodes are used to represent entities stored in base.
552 # Apart from properties and relationships (edges),
553 # nodes can also be labeled with zero or more labels.
555 # A label is a `String` that is used to group nodes into sets.
556 # All nodes labeled with the same label belongs to the same set.
557 # A node may be labeled with any number of labels, including none,
558 # making labels an optional addition to the graph.
560 # Creating new nodes:
562 # var client = new Neo4jClient("http://localhost:7474")
564 # var andres = new NeoNode
565 # andres.labels.add "Person"
566 # andres["name"] = "Andres"
568 # client.save_node(andres)
569 # assert andres.is_linked
571 # Get nodes from Neo4j:
573 # var url = andres.url.to_s
574 # var node = client.load_node(url)
575 # assert node["name"] == "Andres"
576 # assert node["age"].to_s.to_i == 22
580 private var internal_labels
: nullable Array[String] = null
581 private var internal_in_edges
: nullable List[NeoEdge] = null
582 private var internal_out_edges
: nullable List[NeoEdge] = null
586 self.internal_labels
= new Array[String]
587 self.internal_in_edges
= new List[NeoEdge]
588 self.internal_out_edges
= new List[NeoEdge]
592 var tpl
= new FlatBuffer
594 tpl
.append
"labels: [{labels.join(", ")}],"
595 tpl
.append
"data: {properties.to_json}"
597 return tpl
.write_to_string
600 # A label is a `String` that is used to group nodes into sets.
601 # A node may be labeled with any number of labels, including none.
602 # All nodes labeled with the same label belongs to the same set.
604 # Many database queries can work with these sets instead of the whole graph,
605 # making queries easier to write and more efficient.
607 # Labels are loaded lazily
608 fun labels
: Array[String] do return internal_labels
or else load_labels
610 private fun load_labels
: Array[String] do
611 var labels
= new Array[String]
612 var res
= neo
.get
("{url.to_s}/labels")
613 if res
isa JsonArray then
614 for val
in res
do labels
.add val
.to_s
616 internal_labels
= labels
620 # Get the list of `NeoEdge` pointing to `self`
622 # Edges are loaded lazily
623 fun in_edges
: List[NeoEdge] do return internal_in_edges
or else load_in_edges
625 private fun load_in_edges
: List[NeoEdge] do
626 var edges
= new List[NeoEdge]
627 var res
= neo
.get
("{url.to_s}/relationships/in").as(JsonArray)
629 edges
.add
(new NeoEdge.from_json
(neo
, obj
.as(JsonObject)))
631 internal_in_edges
= edges
635 # Get the list of `NeoEdge` pointing from `self`
637 # Edges are loaded lazily
638 fun out_edges
: List[NeoEdge] do return internal_out_edges
or else load_out_edges
640 private fun load_out_edges
: List[NeoEdge] do
641 var edges
= new List[NeoEdge]
642 var res
= neo
.get
("{url.to_s}/relationships/out")
643 for obj
in res
.as(JsonArray) do
644 edges
.add
(new NeoEdge.from_json
(neo
, obj
.as(JsonObject)))
646 internal_out_edges
= edges
650 # Get nodes pointed by `self` following a `rel_type` edge
651 fun out_nodes
(rel_type
: String): Array[NeoNode] do
652 var res
= new Array[NeoNode]
653 for edge
in out_edges
do
654 if edge
.rel_type
== rel_type
then res
.add edge
.to
659 # Get nodes pointing to `self` following a `rel_type` edge
660 fun in_nodes
(rel_type
: String): Array[NeoNode] do
661 var res
= new Array[NeoNode]
662 for edge
in in_edges
do
663 if edge
.rel_type
== rel_type
then res
.add edge
.from
669 # A relationship between two nodes.
670 # Relationships between nodes are a key part of a graph database.
671 # They allow for finding related data. Just like nodes, relationships can have properties.
673 # Create a relationship:
675 # var client = new Neo4jClient("http://localhost:7474")
677 # var andres = new NeoNode
678 # andres["name"] = "Andres"
679 # var kate = new NeoNode
680 # kate["name"] = "Kate"
681 # # Create a relationship of type `LOVES`
682 # var loves = new NeoEdge(andres, "LOVES", kate)
683 # client.save_edge(loves)
684 # assert loves.is_linked
686 # Get an edge from DB:
688 # var url = loves.url.to_s
689 # var edge = client.load_edge(url)
690 # assert edge.from["name"].to_s == "Andres"
691 # assert edge.to["name"].to_s == "Kate"
695 private var internal_from
: nullable NeoNode
696 private var internal_to
: nullable NeoNode
697 private var internal_type
: nullable String
698 private var internal_from_url
: nullable String
699 private var internal_to_url
: nullable String
701 init(from
: NeoNode, rel_type
: String, to
: NeoNode) do
702 self.internal_from
= from
703 self.internal_to
= to
704 self.internal_type
= rel_type
707 redef init from_neo
(neo
, url
) do
709 var obj
= neo
.get
(url
).as(JsonObject)
710 self.internal_type
= obj
["type"].to_s
711 self.internal_from_url
= obj
["start"].to_s
712 self.internal_to_url
= obj
["end"].to_s
715 redef init from_json
(neo
, obj
) do
717 self.internal_type
= obj
["type"].to_s
718 self.internal_from_url
= obj
["start"].to_s
719 self.internal_to_url
= obj
["end"].to_s
723 fun from
: NeoNode do return internal_from
or else load_from
725 private fun load_from
: NeoNode do
726 var node
= neo
.load_node
(internal_from_url
.to_s
)
732 fun to
: NeoNode do return internal_to
or else load_to
734 private fun load_to
: NeoNode do
735 var node
= neo
.load_node
(internal_to_url
.to_s
)
741 fun rel_type
: nullable String do return internal_type
743 # Get the JSON body of a REST request that create the relationship.
744 private fun to_rest
: JsonObject do
745 var obj
= new JsonObject
749 obj
["to"] = "\{{to.batch_id.to_s}\}"
751 obj
["type"] = rel_type
752 obj
["data"] = properties
757 # Batches are used to perform multiple operations on the REST API in one cURL request.
758 # This can significantly improve performance for large insert and update operations.
760 # see: http://docs.neo4j.org/chunked/milestone/rest-api-batch-ops.html
762 # This service is transactional.
763 # If any of the operations performed fails (returns a non-2xx HTTP status code),
764 # the transaction will be rolled back and all changes will be undone.
768 # var client = new Neo4jClient("http://localhost:7474")
770 # var node1 = new NeoNode
771 # var node2 = new NeoNode
772 # var edge = new NeoEdge(node1, "TO", node2)
774 # var batch = new NeoBatch(client)
775 # batch.save_node(node1)
776 # batch.save_node(node2)
777 # batch.save_edge(edge)
780 # assert node1.is_linked
781 # assert node2.is_linked
782 # assert edge.is_linked
785 # Neo4j client connector
786 var client
: Neo4jClient
788 # Jobs to perform in this batch
790 # The batch service expects an array of job descriptions as input,
791 # each job description describing an action to be performed via the normal server API.
792 var jobs
= new HashMap[Int, NeoJob]
794 # Append a new job to the batch in JSON Format
796 fun new_job
(nentity
: NeoEntity): NeoJob do
798 var job
= new NeoJob(id
, nentity
)
803 # Load a node in batch mode also load labels, data and edges
804 fun load_node
(node
: NeoNode) do
805 var job
= new_job
(node
)
806 job
.action
= load_node_data_action
808 if node
.id
!= null then
809 job
.to
= "/node/{node.id.to_s}"
811 job
.to
= "\{{node.batch_id.to_s}\}"
814 job
.action
= load_node_labels_action
816 if node
.id
!= null then
817 job
.to
= "/node/{node.id.to_s}/labels"
819 job
.to
= "\{{node.batch_id.to_s}\}/labels"
823 # Load in and out edges into node
824 fun load_node_edges
(node
: NeoNode) do
825 var job
= new_job
(node
)
826 job
.action
= load_node_in_edges_action
828 if node
.id
!= null then
829 job
.to
= "/node/{node.id.to_s}/relationships/in"
831 job
.to
= "\{{node.batch_id.to_s}\}/relationships/in"
834 job
.action
= load_node_out_edges_action
836 if node
.id
!= null then
837 job
.to
= "/node/{node.id.to_s}/relationships/out"
839 job
.to
= "\{{node.batch_id.to_s}\}/relationships/out"
843 # Create a `NeoNode` or a `NeoEdge` in batch mode.
844 fun save_entity
(nentity
: NeoEntity) do
845 if nentity
isa NeoNode then
847 else if nentity
isa NeoEdge then
852 # Create a node in batch mode also create labels and edges
853 fun save_node
(node
: NeoNode) do
854 if node
.id
!= null or node
.batch_id
!= null then return
856 var job
= new_job
(node
)
857 node
.batch_id
= job
.id
858 job
.action
= create_node_action
861 job
.body
= node
.properties
865 job
.to
= "\{{node.batch_id.to_s}\}/labels"
866 job
.body
= new JsonArray.from
(node
.labels
)
868 #save_edges(node.out_edges)
871 # Create multiple nodes
872 # also create labels and edges
873 fun save_nodes
(nodes
: Collection[NeoNode]) do for node
in nodes
do save_node
(node
)
876 # nodes `edge.from` and `edge.to` will be created if not in base
877 fun save_edge
(edge
: NeoEdge) do
878 if edge
.id
!= null or edge
.batch_id
!= null then return
883 var job
= new_job
(edge
)
884 edge
.batch_id
= job
.id
885 job
.action
= create_edge_action
887 if edge
.from
.id
!= null then
888 job
.to
= "/node/{edge.from.id.to_s}/relationships"
890 job
.to
= "\{{edge.from.batch_id.to_s}\}/relationships"
892 job
.body
= edge
.to_rest
895 # Create multiple edges
896 fun save_edges
(edges
: Collection[NeoEdge]) do for edge
in edges
do save_edge
(edge
)
898 # Execute the batch and update local nodes
899 fun execute
: List[JsonError] do
900 var request
= new JsonPOST(client
.batch_url
, client
.curl
)
901 # request.headers["X-Stream"] = "true"
902 var json_jobs
= new JsonArray
903 for job
in jobs
.values
do json_jobs
.add job
.to_rest
904 request
.data
= json_jobs
905 var response
= request
.execute
906 var res
= client
.parse_response
(response
)
907 return finalize_batch
(res
)
910 # Associate data from response in original nodes and edges
911 private fun finalize_batch
(response
: Jsonable): List[JsonError] do
912 var errors
= new List[JsonError]
913 if not response
isa JsonArray then
914 errors
.add
(new JsonError("Neo4jError", "Unexpected batch response format"))
917 # print " {res.length} jobs executed"
918 for res
in response
do
919 if not res
isa JsonObject then
920 errors
.add
(new JsonError("Neo4jError", "Unexpected job format in batch response"))
923 var id
= res
["id"].as(Int)
925 if job
.action
== create_node_action
then
926 var node
= job
.entity
.as(NeoNode)
928 node
.url
= res
["location"].to_s
929 else if job
.action
== create_edge_action
then
930 var edge
= job
.entity
.as(NeoEdge)
932 edge
.url
= res
["location"].to_s
933 else if job
.action
== load_node_data_action
then
934 var node
= job
.entity
.as(NeoNode)
935 node
.internal_properties
= res
["body"].as(JsonObject)["data"].as(JsonObject)
936 else if job
.action
== load_node_labels_action
then
937 var node
= job
.entity
.as(NeoNode)
938 var labels
= new Array[String]
939 for l
in res
["body"].as(JsonArray) do labels
.add l
.to_s
940 node
.internal_labels
= labels
941 else if job
.action
== load_node_in_edges_action
then
942 var node
= job
.entity
.as(NeoNode)
943 var edges
= res
["body"].as(JsonArray)
944 node
.internal_in_edges
= new List[NeoEdge]
946 node
.internal_in_edges
.add client
.load_edge
(edge
.as(JsonObject)["self"].to_s
)
948 else if job
.action
== load_node_out_edges_action
then
949 var node
= job
.entity
.as(NeoNode)
950 var edges
= res
["body"].as(JsonArray)
951 node
.internal_out_edges
= new List[NeoEdge]
953 node
.internal_out_edges
.add client
.load_edge
(edge
.as(JsonObject)["self"].to_s
)
961 # TODO replace with enum
963 private fun create_node_action
: Int do return 1
964 private fun create_edge_action
: Int do return 2
965 private fun load_node_data_action
: Int do return 3
966 private fun load_node_labels_action
: Int do return 4
967 private fun load_node_in_edges_action
: Int do return 5
968 private fun load_node_out_edges_action
: Int do return 6
971 # A job that can be executed in a `NeoBatch`
972 # This is a representation of a neo job in JSON Format
974 # Each job description should contain a `to` attribute, with a value relative to the data API root
975 # (so http://localhost:7474/db/data/node becomes just /node), and a `method` attribute containing
978 # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track
979 # of responses, although responses are guaranteed to be returned in the same order the job
980 # descriptions are received.
984 # Entity targeted by the job
985 var entity
: NeoEntity
987 init(id
: Int, entity
: NeoEntity) do
992 # What kind of action do the job
993 # used to attach responses to original Neo objets
994 private var action
: nullable Int = null
996 # Job HTTP method: `GET`, `POST`, `PUT`, `DELETE`...
998 # Job service target: `/node`, `/labels` etc...
1000 # Body to send with the job service request
1001 var body
: nullable Jsonable = null
1004 fun to_rest
: JsonObject do
1005 var job
= new JsonObject
1007 job
["method"] = method
1009 if not body
== null then