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
69 # Handles Neo4j server start and stop command
71 # `neo4j` binary must be in `PATH` in order to work
74 # Start the local Neo4j server instance
76 sys
.system
("neo4j start console")
80 # Like `start` but redirect the console output to `/dev/null`
81 fun start_quiet
: Bool do
82 sys
.system
("neo4j start console > /dev/null")
86 # Stop the local Neo4j server instance
88 sys
.system
("neo4j stop")
92 # Like `stop` but redirect the console output to `/dev/null`
93 fun stop_quiet
: Bool do
94 sys
.system
("neo4j stop > /dev/null")
99 # `Neo4jClient` is needed to communicate through the REST API
101 # var client = new Neo4jClient("http://localhost:7474")
102 # assert client.is_ok
105 # Neo4j REST services baseurl
107 # REST service to get node data
108 private var node_url
: String
109 # REST service to batch
110 private var batch_url
: String
111 # REST service to send cypher requests
112 private var cypher_url
: String
114 private var curl
= new Curl
116 init(base_url
: String) do
117 self.base_url
= base_url
118 var root
= service_root
119 assert root
isa JsonObject else
120 sys
.stderr
.write
"Neo4jClientError: cannot connect to server at <{base_url}>.\n"
122 self.node_url
= root
["node"].to_s
123 self.batch_url
= root
["batch"].to_s
124 self.cypher_url
= root
["cypher"].to_s
127 fun service_root
: Serializable do return get
(base_url
/ "db/data")
129 # Is the connection with the Neo4j server ok?
130 fun is_ok
: Bool do return service_root
isa JsonObject
134 cypher
(new CypherQuery.from_string
("MATCH (n) OPTIONAL MATCH n-[r]-() DELETE r, n"))
138 var errors
= new Array[String]
140 # Nodes view stored locally
141 private var local_nodes
= new HashMap[String, nullable NeoNode]
143 # Save the node in base
145 # var client = new Neo4jClient("http://localhost:7474")
148 # var andres = new NeoNode
149 # andres["name"] = "Andres"
150 # client.save_node(andres)
151 # assert andres.is_linked
153 # Once linked, nodes cannot be created twice:
155 # var oldurl = andres.url
156 # client.save_node(andres) # do nothing
157 # assert andres.url == oldurl
158 fun save_node
(node
: NeoNode): Bool do
159 if node
.is_linked
then return true
161 var batch
= new NeoBatch(self)
162 batch
.save_node
(node
)
163 # batch.create_edges(node.out_edges)
164 var errors
= batch
.execute
165 if not errors
.is_empty
then
166 errors
.add_all errors
169 local_nodes
[node
.url
.to_s
] = node
173 # Load a node from base
174 # Data, labels and edges will be loaded lazily.
175 fun load_node
(url
: String): NeoNode do
176 if local_nodes
.has_key
(url
) then
177 var node
= local_nodes
[url
]
178 if node
!= null then return node
180 var node
= new NeoNode.from_neo
(self, url
)
181 local_nodes
[url
] = node
185 # Remove the entity from base
186 fun delete_node
(node
: NeoNode): Bool do
187 if not node
.is_linked
then return false
188 var url
= node
.url
.to_s
190 local_nodes
[url
] = null
195 # Edges view stored locally
196 private var local_edges
= new HashMap[String, nullable NeoEdge]
198 # Save the edge in base
199 # From and to nodes will be created.
201 # var client = new Neo4jClient("http://localhost:7474")
203 # var andres = new NeoNode
204 # var kate = new NeoNode
205 # var edge = new NeoEdge(andres, "LOVES", kate)
206 # client.save_edge(edge)
207 # assert andres.is_linked
208 # assert kate.is_linked
209 # assert edge.is_linked
210 fun save_edge
(edge
: NeoEdge): Bool do
211 if edge
.is_linked
then return true
213 edge
.from
.out_edges
.add edge
214 edge
.to
.in_edges
.add edge
215 var batch
= new NeoBatch(self)
216 batch
.save_edge
(edge
)
217 var errors
= batch
.execute
218 if not errors
.is_empty
then
219 errors
.add_all errors
222 local_edges
[edge
.url
.to_s
] = edge
226 # Load a edge from base
227 # Data will be loaded lazily.
228 fun load_edge
(url
: String): NeoEdge do
229 if local_edges
.has_key
(url
) then
230 var node
= local_edges
[url
]
231 if node
!= null then return node
233 var edge
= new NeoEdge.from_neo
(self, url
)
234 local_edges
[url
] = edge
238 # Remove the edge from base
239 fun delete_edge
(edge
: NeoEdge): Bool do
240 if not edge
.is_linked
then return false
241 var url
= edge
.url
.to_s
243 local_edges
[url
] = null
248 # Retrieve all nodes with specified `lbl`
250 # var client = new Neo4jClient("http://localhost:7474")
252 # var andres = new NeoNode
253 # andres.labels.add_all(["Human", "Male"])
254 # client.save_node(andres)
255 # var kate = new NeoNode
256 # kate.labels.add_all(["Human", "Female"])
257 # client.save_node(kate)
259 # var nodes = client.nodes_with_label("Human")
260 # assert nodes.has(andres)
261 # assert nodes.has(kate)
262 fun nodes_with_label
(lbl
: String): Array[NeoNode] do
263 var res
= get
(base_url
/ "db/data/label/{lbl.to_percent_encoding}/nodes")
264 var nodes
= new Array[NeoNode]
265 for json
in res
.as(JsonArray) do
266 var obj
= json
.as(JsonObject)
267 var node
= load_node
(obj
["self"].to_s
)
268 node
.internal_properties
= obj
["data"].as(JsonObject)
274 # Retrieve nodes belonging to all the specified `labels`.
276 # var client = new Neo4jClient("http://localhost:7474")
278 # var andres = new NeoNode
279 # andres.labels.add_all(["Human", "Male"])
280 # client.save_node(andres)
281 # var kate = new NeoNode
282 # kate.labels.add_all(["Human", "Female"])
283 # client.save_node(kate)
285 # var nodes = client.nodes_with_labels(["Human", "Male"])
286 # assert nodes.has(andres)
287 # assert not nodes.has(kate)
288 fun nodes_with_labels
(labels
: Array[String]): Array[NeoNode] do
289 assert not labels
.is_empty
292 var buffer
= new Buffer
293 buffer
.append
"match n where \{label_0\} in labels(n)"
294 for i
in [1..labels
.length
[ do
295 buffer
.append
" and \{label_{i}\} in labels(n)"
297 buffer
.append
" return n"
298 var query
= new CypherQuery.from_string
(buffer
.write_to_string
)
299 for i
in [0..labels
.length
[ do
300 query
.params
["label_{i}"] = labels
[i
]
303 # Retrieve the answer.
304 var res
= cypher
(query
)
305 var nodes
= new Array[NeoNode]
306 for json
in res
.as(JsonObject)["data"].as(JsonArray) do
307 var obj
= json
.as(JsonArray).first
.as(JsonObject)
308 var node
= load_node
(obj
["self"].to_s
)
309 node
.internal_properties
= obj
["data"].as(JsonObject)
315 # Perform a `CypherQuery`
317 fun cypher
(query
: CypherQuery): Serializable do
318 return post
("{cypher_url}", query
.to_rest
)
321 # GET JSON data from `url`
322 fun get
(url
: String): Serializable do
323 var request
= new JsonGET(url
)
324 var response
= request
.execute
325 return parse_response
(response
)
328 # POST `params` to `url`
329 fun post
(url
: String, params
: Serializable): Serializable do
330 var request
= new JsonPOST(url
)
331 request
.json_data
= params
332 var response
= request
.execute
333 return parse_response
(response
)
336 # PUT `params` at `url`
337 fun put
(url
: String, params
: Serializable): Serializable do
338 var request
= new JsonPUT(url
)
339 request
.json_data
= params
340 var response
= request
.execute
341 return parse_response
(response
)
345 fun delete
(url
: String): Serializable do
346 var request
= new JsonDELETE(url
)
347 var response
= request
.execute
348 return parse_response
(response
)
351 # Parse the cURL `response` as a JSON string
352 private fun parse_response
(response
: CurlResponse): Serializable do
353 if response
isa CurlResponseSuccess then
354 var str
= response
.body_str
355 if str
.is_empty
then return new JsonObject
356 var res
= str
.parse_json
357 if res
isa JsonParseError then
358 var e
= new NeoError(res
.to_s
, "JsonParseError")
363 # empty response wrap it in empty object
364 return new JsonObject
365 else if res
isa JsonObject and res
.has_key
("exception") then
366 var error
= "Neo4jError::{res["exception"] or else "null"}"
368 if res
.has_key
("message") then
369 msg
= res
["message"].to_s
371 return new NeoError(msg
, error
)
375 else if response
isa CurlResponseFailed then
376 return new NeoError("{response.error_msg} ({response.error_code})", "CurlError")
378 return new NeoError("Unexpected response \"{response}\
".", "CurlError")
383 # A Cypher query for Neo4j REST API
385 # The Neo4j REST API allows querying with Cypher.
386 # The results are returned as a list of string headers (columns), and a data part,
387 # consisting of a list of all rows, every row consisting of a list of REST representations
388 # of the field value - Node, Relationship, Path or any simple value like String.
392 # var client = new Neo4jClient("http://localhost:7474")
393 # var query = new CypherQuery
394 # query.nmatch("(n)-[r:LOVES]->(m)")
395 # query.nwhere("n.name=\"Andres\"")
396 # query.nreturn("m.name")
397 # var res = client.cypher(query).as(JsonObject)
398 # assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
400 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
402 # Query string to perform
403 private var query
: String = ""
405 # `params` to embed in the query like in prepared statements
406 var params
= new JsonObject
408 # init the query from a query string
409 init from_string
(query
: String) do
413 # init the query with parameters
414 init with_params
(params
: JsonObject) do
418 # Pass the argument `value` as the parameter `key`.
421 fun []=(key
: String, value
: nullable Serializable) do
425 # Add a `CREATE` statement to the query
426 fun ncreate
(query
: String): CypherQuery do
427 self.query
= "{self.query}CREATE {query} "
431 # Add a `START` statement to the query
432 fun nstart
(query
: String): CypherQuery do
433 self.query
= "{self.query}START {query} "
437 # Add a `MATCH` statement to the query
438 fun nmatch
(query
: String): CypherQuery do
439 self.query
= "{self.query}MATCH {query} "
443 # Add a `WHERE` statement to the query
444 fun nwhere
(query
: String): CypherQuery do
445 self.query
= "{self.query}WHERE {query} "
449 # Add a `AND` statement to the query
450 fun nand
(query
: String): CypherQuery do
451 self.query
= "{self.query}AND {query} "
455 # Add a `RETURN` statement to the query
456 fun nreturn
(query
: String): CypherQuery do
457 self.query
= "{self.query}RETURN {query} "
461 # Pass the argument `value` as the parameter `key`.
466 # var query = (new CypherQuery).nmatch("(n)").nwhere(
467 # "n.key = key").set("key", "foo")
469 # assert query.params["key"] == "foo"
473 fun set
(key
: String, value
: nullable Serializable): SELF do
478 # Translate the query to the body of a corresponding Neo4j REST request.
479 fun to_rest
: JsonObject do
480 var obj
= new JsonObject
482 if not params
.is_empty
then
483 obj
["params"] = params
488 redef fun to_s
do return to_rest
.to_s
491 # The fundamental units that form a graph are nodes and relationships.
493 # Entities can have two states:
495 # * linked: the NeoEntity references an existing node or edge in Neo4j
496 # * unlinked: the NeoEntity is not yet created in Neo4j
498 # If the entity is initialized unlinked from neo4j:
500 # # Create a disconnected node
501 # var andres = new NeoNode
502 # andres["name"] = "Andres"
503 # # At this point, the node is not linked
504 # assert not andres.is_linked
506 # Then we can link the entity to the base:
509 # var client = new Neo4jClient("http://localhost:7474")
510 # client.save_node(andres)
511 # # The node is now linked
512 # assert andres.is_linked
514 # Entities can also be loaded from Neo4j:
516 # # Get a node from Neo4j
517 # var url = andres.url.to_s
518 # var node = client.load_node(url)
519 # assert node.is_linked
521 # When working in connected mode, all reading operations are executed lazily on the base:
523 # # Get the node `name` property
524 # assert node["name"] == "Andres" # loaded lazily from base
525 abstract class NeoEntity
526 # Neo4j client connector
527 private var neo
: Neo4jClient is noinit
529 # Entity unique URL in Neo4j REST API
530 var url
: nullable String = null
532 # Temp id used in batch mode to update the entity
533 private var batch_id
: nullable Int = null
535 # Load the entity from base
536 private init from_neo
(neo
: Neo4jClient, url
: String) is nosuper
do
541 # Init entity from JSON representation
542 private init from_json
(neo
: Neo4jClient, obj
: JsonObject) is nosuper
do
544 self.url
= obj
["self"].to_s
545 self.internal_properties
= obj
["data"].as(JsonObject)
548 # Create a empty (and not-connected) entity
550 self.internal_properties
= new JsonObject
553 # Is the entity linked to a Neo4j database?
554 fun is_linked
: Bool do return url
!= null
556 # In Neo4j, both nodes and relationships can contain properties.
557 # Properties are key-value pairs where the key is a string.
558 # Property values are JSON formatted.
560 # Properties are loaded lazily
561 fun properties
: JsonObject do return internal_properties
or else load_properties
563 private var internal_properties
: nullable JsonObject = null
565 private fun load_properties
: JsonObject do
566 var obj
= neo
.get
(url
.to_s
/ "properties").as(JsonObject)
567 internal_properties
= obj
571 # Get the entity `id` if connected to base
572 fun id
: nullable Int do
573 if url
== null then return null
574 return url
.split
("/").last
.to_i
577 # Get the entity property at `key`
578 fun [](key
: String): nullable Serializable do
579 if not properties
.has_key
(key
) then return null
580 return properties
[key
]
583 # Set the entity property `value` at `key`
584 fun []=(key
: String, value
: nullable Serializable) do properties
[key
] = value
586 # Is the property `key` set?
587 fun has_key
(key
: String): Bool do return properties
.has_key
(key
)
590 # Nodes are used to represent entities stored in base.
591 # Apart from properties and relationships (edges),
592 # nodes can also be labeled with zero or more labels.
594 # A label is a `String` that is used to group nodes into sets.
595 # All nodes labeled with the same label belongs to the same set.
596 # A node may be labeled with any number of labels, including none,
597 # making labels an optional addition to the graph.
599 # Creating new nodes:
601 # var client = new Neo4jClient("http://localhost:7474")
603 # var andres = new NeoNode
604 # andres.labels.add "Person"
605 # andres["name"] = "Andres"
607 # client.save_node(andres)
608 # assert andres.is_linked
610 # Get nodes from Neo4j:
612 # var url = andres.url.to_s
613 # var node = client.load_node(url)
614 # assert node["name"] == "Andres"
615 # assert node["age"].to_s.to_i == 22
619 private var internal_labels
: nullable Array[String] = null
620 private var internal_in_edges
: nullable List[NeoEdge] = null
621 private var internal_out_edges
: nullable List[NeoEdge] = null
625 self.internal_labels
= new Array[String]
626 self.internal_in_edges
= new List[NeoEdge]
627 self.internal_out_edges
= new List[NeoEdge]
631 var tpl
= new FlatBuffer
633 tpl
.append
"labels: [{labels.join(", ")}],"
634 tpl
.append
"data: {properties.to_json}"
636 return tpl
.write_to_string
639 # A label is a `String` that is used to group nodes into sets.
640 # A node may be labeled with any number of labels, including none.
641 # All nodes labeled with the same label belongs to the same set.
643 # Many database queries can work with these sets instead of the whole graph,
644 # making queries easier to write and more efficient.
646 # Labels are loaded lazily
647 fun labels
: Array[String] do return internal_labels
or else load_labels
649 private fun load_labels
: Array[String] do
650 var labels
= new Array[String]
651 var res
= neo
.get
(url
.to_s
/ "labels")
652 if res
isa JsonArray then
653 for val
in res
do labels
.add val
.to_s
655 internal_labels
= labels
659 # Get the list of `NeoEdge` pointing to `self`
661 # Edges are loaded lazily
662 fun in_edges
: List[NeoEdge] do return internal_in_edges
or else load_in_edges
664 private fun load_in_edges
: List[NeoEdge] do
665 var edges
= new List[NeoEdge]
666 var res
= neo
.get
(url
.to_s
/ "relationships/in").as(JsonArray)
668 edges
.add
(new NeoEdge.from_json
(neo
, obj
.as(JsonObject)))
670 internal_in_edges
= edges
674 # Get the list of `NeoEdge` pointing from `self`
676 # Edges are loaded lazily
677 fun out_edges
: List[NeoEdge] do return internal_out_edges
or else load_out_edges
679 private fun load_out_edges
: List[NeoEdge] do
680 var edges
= new List[NeoEdge]
681 var res
= neo
.get
(url
.to_s
/ "relationships/out")
682 for obj
in res
.as(JsonArray) do
683 edges
.add
(new NeoEdge.from_json
(neo
, obj
.as(JsonObject)))
685 internal_out_edges
= edges
689 # Get nodes pointed by `self` following a `rel_type` edge
690 fun out_nodes
(rel_type
: String): Array[NeoNode] do
691 var res
= new Array[NeoNode]
692 for edge
in out_edges
do
693 if edge
.rel_type
== rel_type
then res
.add edge
.to
698 # Get nodes pointing to `self` following a `rel_type` edge
699 fun in_nodes
(rel_type
: String): Array[NeoNode] do
700 var res
= new Array[NeoNode]
701 for edge
in in_edges
do
702 if edge
.rel_type
== rel_type
then res
.add edge
.from
708 # A relationship between two nodes.
709 # Relationships between nodes are a key part of a graph database.
710 # They allow for finding related data. Just like nodes, relationships can have properties.
712 # Create a relationship:
714 # var client = new Neo4jClient("http://localhost:7474")
716 # var andres = new NeoNode
717 # andres["name"] = "Andres"
718 # var kate = new NeoNode
719 # kate["name"] = "Kate"
720 # # Create a relationship of type `LOVES`
721 # var loves = new NeoEdge(andres, "LOVES", kate)
722 # client.save_edge(loves)
723 # assert loves.is_linked
725 # Get an edge from DB:
727 # var url = loves.url.to_s
728 # var edge = client.load_edge(url)
729 # assert edge.from["name"].to_s == "Andres"
730 # assert edge.to["name"].to_s == "Kate"
734 private var internal_from
: nullable NeoNode
735 private var internal_to
: nullable NeoNode
736 private var internal_type
: nullable String
737 private var internal_from_url
: nullable String
738 private var internal_to_url
: nullable String
740 init(from
: NeoNode, rel_type
: String, to
: NeoNode) do
741 self.internal_from
= from
742 self.internal_to
= to
743 self.internal_type
= rel_type
746 redef init from_neo
(neo
, url
) do
748 var obj
= neo
.get
(url
).as(JsonObject)
749 self.internal_type
= obj
["type"].to_s
750 self.internal_from_url
= obj
["start"].to_s
751 self.internal_to_url
= obj
["end"].to_s
754 redef init from_json
(neo
, obj
) do
756 self.internal_type
= obj
["type"].to_s
757 self.internal_from_url
= obj
["start"].to_s
758 self.internal_to_url
= obj
["end"].to_s
762 fun from
: NeoNode do return internal_from
or else load_from
764 private fun load_from
: NeoNode do
765 var node
= neo
.load_node
(internal_from_url
.to_s
)
771 fun to
: NeoNode do return internal_to
or else load_to
773 private fun load_to
: NeoNode do
774 var node
= neo
.load_node
(internal_to_url
.to_s
)
780 fun rel_type
: nullable String do return internal_type
782 # Get the JSON body of a REST request that create the relationship.
783 private fun to_rest
: JsonObject do
784 var obj
= new JsonObject
788 obj
["to"] = "\{{to.batch_id.to_s}\}"
790 obj
["type"] = rel_type
791 obj
["data"] = properties
796 # Batches are used to perform multiple operations on the REST API in one cURL request.
797 # This can significantly improve performance for large insert and update operations.
799 # see: http://docs.neo4j.org/chunked/milestone/rest-api-batch-ops.html
801 # This service is transactional.
802 # If any of the operations performed fails (returns a non-2xx HTTP status code),
803 # the transaction will be rolled back and all changes will be undone.
807 # var client = new Neo4jClient("http://localhost:7474")
809 # var node1 = new NeoNode
810 # var node2 = new NeoNode
811 # var edge = new NeoEdge(node1, "TO", node2)
813 # var batch = new NeoBatch(client)
814 # batch.save_node(node1)
815 # batch.save_node(node2)
816 # batch.save_edge(edge)
819 # assert node1.is_linked
820 # assert node2.is_linked
821 # assert edge.is_linked
824 # Neo4j client connector
825 var client
: Neo4jClient
827 # Jobs to perform in this batch
829 # The batch service expects an array of job descriptions as input,
830 # each job description describing an action to be performed via the normal server API.
831 var jobs
= new HashMap[Int, NeoJob]
833 # Append a new job to the batch in JSON Format
835 fun new_job
(nentity
: NeoEntity): NeoJob do
837 var job
= new NeoJob(id
, nentity
)
842 # Load a node in batch mode also load labels, data and edges
843 fun load_node
(node
: NeoNode) do
844 var job
= new_job
(node
)
845 job
.action
= load_node_data_action
847 if node
.id
!= null then
848 job
.to
= "/node/{node.id.to_s}"
850 job
.to
= "\{{node.batch_id.to_s}\}"
853 job
.action
= load_node_labels_action
855 if node
.id
!= null then
856 job
.to
= "/node/{node.id.to_s}/labels"
858 job
.to
= "\{{node.batch_id.to_s}\}/labels"
862 # Load in and out edges into node
863 fun load_node_edges
(node
: NeoNode) do
864 var job
= new_job
(node
)
865 job
.action
= load_node_in_edges_action
867 if node
.id
!= null then
868 job
.to
= "/node/{node.id.to_s}/relationships/in"
870 job
.to
= "\{{node.batch_id.to_s}\}/relationships/in"
873 job
.action
= load_node_out_edges_action
875 if node
.id
!= null then
876 job
.to
= "/node/{node.id.to_s}/relationships/out"
878 job
.to
= "\{{node.batch_id.to_s}\}/relationships/out"
882 # Create a `NeoNode` or a `NeoEdge` in batch mode.
883 fun save_entity
(nentity
: NeoEntity) do
884 if nentity
isa NeoNode then
886 else if nentity
isa NeoEdge then
891 # Create a node in batch mode also create labels and edges
892 fun save_node
(node
: NeoNode) do
893 if node
.id
!= null or node
.batch_id
!= null then return
895 var job
= new_job
(node
)
896 node
.batch_id
= job
.id
897 job
.action
= create_node_action
900 job
.body
= node
.properties
904 job
.to
= "\{{node.batch_id.to_s}\}/labels"
905 job
.body
= new JsonArray.from
(node
.labels
)
907 #save_edges(node.out_edges)
910 # Create multiple nodes
911 # also create labels and edges
912 fun save_nodes
(nodes
: Collection[NeoNode]) do for node
in nodes
do save_node
(node
)
915 # nodes `edge.from` and `edge.to` will be created if not in base
916 fun save_edge
(edge
: NeoEdge) do
917 if edge
.id
!= null or edge
.batch_id
!= null then return
922 var job
= new_job
(edge
)
923 edge
.batch_id
= job
.id
924 job
.action
= create_edge_action
926 if edge
.from
.id
!= null then
927 job
.to
= "/node/{edge.from.id.to_s}/relationships"
929 job
.to
= "\{{edge.from.batch_id.to_s}\}/relationships"
931 job
.body
= edge
.to_rest
934 # Create multiple edges
935 fun save_edges
(edges
: Collection[NeoEdge]) do for edge
in edges
do save_edge
(edge
)
937 # Execute the batch and update local nodes
938 fun execute
: List[NeoError] do
939 var request
= new JsonPOST(client
.batch_url
)
940 # request.headers["X-Stream"] = "true"
941 var json_jobs
= new JsonArray
942 for job
in jobs
.values
do json_jobs
.add job
.to_rest
943 request
.json_data
= json_jobs
944 var response
= request
.execute
945 var res
= client
.parse_response
(response
)
946 return finalize_batch
(res
)
949 # Associate data from response in original nodes and edges
950 private fun finalize_batch
(response
: Serializable): List[NeoError] do
951 var errors
= new List[NeoError]
952 if not response
isa JsonArray then
953 errors
.add
(new NeoError("Unexpected batch response format.", "Neo4jError"))
956 # print " {res.length} jobs executed"
957 for res
in response
do
958 if not res
isa JsonObject then
959 errors
.add
(new NeoError("Unexpected job format in batch response.", "Neo4jError"))
962 var id
= res
["id"].as(Int)
964 if job
.action
== create_node_action
then
965 var node
= job
.entity
.as(NeoNode)
967 node
.url
= res
["location"].to_s
968 else if job
.action
== create_edge_action
then
969 var edge
= job
.entity
.as(NeoEdge)
971 edge
.url
= res
["location"].to_s
972 else if job
.action
== load_node_data_action
then
973 var node
= job
.entity
.as(NeoNode)
974 node
.internal_properties
= res
["body"].as(JsonObject)["data"].as(JsonObject)
975 else if job
.action
== load_node_labels_action
then
976 var node
= job
.entity
.as(NeoNode)
977 var labels
= new Array[String]
978 for l
in res
["body"].as(JsonArray) do labels
.add l
.to_s
979 node
.internal_labels
= labels
980 else if job
.action
== load_node_in_edges_action
then
981 var node
= job
.entity
.as(NeoNode)
982 var edges
= res
["body"].as(JsonArray)
983 node
.internal_in_edges
= new List[NeoEdge]
985 node
.internal_in_edges
.add client
.load_edge
(edge
.as(JsonObject)["self"].to_s
)
987 else if job
.action
== load_node_out_edges_action
then
988 var node
= job
.entity
.as(NeoNode)
989 var edges
= res
["body"].as(JsonArray)
990 node
.internal_out_edges
= new List[NeoEdge]
992 node
.internal_out_edges
.add client
.load_edge
(edge
.as(JsonObject)["self"].to_s
)
1000 # TODO replace with enum
1002 private fun create_node_action
: Int do return 1
1003 private fun create_edge_action
: Int do return 2
1004 private fun load_node_data_action
: Int do return 3
1005 private fun load_node_labels_action
: Int do return 4
1006 private fun load_node_in_edges_action
: Int do return 5
1007 private fun load_node_out_edges_action
: Int do return 6
1010 # A job that can be executed in a `NeoBatch`
1011 # This is a representation of a neo job in JSON Format
1013 # Each job description should contain a `to` attribute, with a value relative to the data API root
1014 # (so http://localhost:7474/db/data/node becomes just /node), and a `method` attribute containing
1017 # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track
1018 # of responses, although responses are guaranteed to be returned in the same order the job
1019 # descriptions are received.
1023 # Entity targeted by the job
1024 var entity
: NeoEntity
1026 init(id
: Int, entity
: NeoEntity) do
1028 self.entity
= entity
1031 # What kind of action do the job
1032 # used to attach responses to original Neo objets
1033 private var action
: nullable Int = null
1035 # Job HTTP method: `GET`, `POST`, `PUT`, `DELETE`...
1037 # Job service target: `/node`, `/labels` etc...
1039 # Body to send with the job service request
1040 var body
: nullable Serializable = null
1043 fun to_rest
: JsonObject do
1044 var job
= new JsonObject
1046 job
["method"] = method
1048 if not body
== null then