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 # In order to connect to Neo4j you need a connector:
19 # # Create new Neo4j client
20 # var client = new Neo4jClient("http://neo4j:7474")
23 # The fundamental units that form a graph are nodes and relationships.
25 # Nodes are used to represent entities stored in base:
27 # # Create a disconnected node
28 # var andres = new NeoNode
29 # andres["name"] = "Andres"
30 # # Connect the node to Neo4j
31 # client.save_node(andres)
32 # assert andres.is_linked
34 # # Create a second node
35 # var kate = new NeoNode
36 # kate["name"] = "Kate"
37 # client.save_node(kate)
38 # assert kate.is_linked
40 # Relationships between nodes are a key part of a graph database.
41 # They allow for finding related data. Just like nodes, relationships can have properties.
43 # # Create a relationship
44 # var loves = new NeoEdge(andres, "LOVES", kate)
45 # client.save_edge(loves)
46 # assert loves.is_linked
48 # Nodes can also be loaded fron Neo4j:
50 # # Get a node from DB and explore edges
51 # var url = andres.url.to_s
52 # var from = client.load_node(url)
53 # assert from["name"].to_s == "Andres"
54 # var to = from.out_nodes("LOVES").first # follow the first LOVES relationship
55 # assert to["name"].to_s == "Kate"
57 # For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html
63 # `Neo4jClient` is needed to communicate through the REST API
65 # var client = new Neo4jClient("http://neo4j:7474")
69 # Neo4j REST services baseurl
71 # REST service to get node data
72 private var node_url
: String
73 # REST service to batch
74 private var batch_url
: String
75 # REST service to send cypher requests
76 private var cypher_url
: String
78 private var curl
= new Curl
80 init(base_url
: String) do
81 self.base_url
= base_url
82 var root
= service_root
83 assert root
isa JsonObject else
84 sys
.stderr
.write
"Neo4jClientError: cannot connect to server at <{base_url}>.\n"
86 self.node_url
= root
["node"].to_s
87 self.batch_url
= root
["batch"].to_s
88 self.cypher_url
= root
["cypher"].to_s
91 fun service_root
: Serializable do return get
(base_url
/ "db/data")
93 # Is the connection with the Neo4j server ok?
94 fun is_ok
: Bool do return service_root
isa JsonObject
98 cypher
(new CypherQuery.from_string
("MATCH (n) OPTIONAL MATCH n-[r]-() DELETE r, n"))
102 var errors
= new Array[String]
104 # Nodes view stored locally
105 private var local_nodes
= new HashMap[String, nullable NeoNode]
107 # Save the node in base
109 # var client = new Neo4jClient("http://neo4j:7474")
112 # var andres = new NeoNode
113 # andres["name"] = "Andres"
114 # client.save_node(andres)
115 # assert andres.is_linked
117 # Once linked, nodes cannot be created twice:
119 # var oldurl = andres.url
120 # client.save_node(andres) # do nothing
121 # assert andres.url == oldurl
122 fun save_node
(node
: NeoNode): Bool do
123 if node
.is_linked
then return true
125 var batch
= new NeoBatch(self)
126 batch
.save_node
(node
)
127 # batch.create_edges(node.out_edges)
128 var errors
= batch
.execute
129 if not errors
.is_empty
then
130 errors
.add_all errors
133 local_nodes
[node
.url
.to_s
] = node
137 # Load a node from base
138 # Data, labels and edges will be loaded lazily.
139 fun load_node
(url
: String): NeoNode do
140 if local_nodes
.has_key
(url
) then
141 var node
= local_nodes
[url
]
142 if node
!= null then return node
144 var node
= new NeoNode.from_neo
(self, url
)
145 local_nodes
[url
] = node
149 # Remove the entity from base
150 fun delete_node
(node
: NeoNode): Bool do
151 if not node
.is_linked
then return false
152 var url
= node
.url
.to_s
154 local_nodes
[url
] = null
159 # Edges view stored locally
160 private var local_edges
= new HashMap[String, nullable NeoEdge]
162 # Save the edge in base
163 # From and to nodes will be created.
165 # var client = new Neo4jClient("http://neo4j:7474")
167 # var andres = new NeoNode
168 # var kate = new NeoNode
169 # var edge = new NeoEdge(andres, "LOVES", kate)
170 # client.save_edge(edge)
171 # assert andres.is_linked
172 # assert kate.is_linked
173 # assert edge.is_linked
174 fun save_edge
(edge
: NeoEdge): Bool do
175 if edge
.is_linked
then return true
177 edge
.from
.out_edges
.add edge
178 edge
.to
.in_edges
.add edge
179 var batch
= new NeoBatch(self)
180 batch
.save_edge
(edge
)
181 var errors
= batch
.execute
182 if not errors
.is_empty
then
183 errors
.add_all errors
186 local_edges
[edge
.url
.to_s
] = edge
190 # Load a edge from base
191 # Data will be loaded lazily.
192 fun load_edge
(url
: String): NeoEdge do
193 if local_edges
.has_key
(url
) then
194 var node
= local_edges
[url
]
195 if node
!= null then return node
197 var edge
= new NeoEdge.from_neo
(self, url
)
198 local_edges
[url
] = edge
202 # Remove the edge from base
203 fun delete_edge
(edge
: NeoEdge): Bool do
204 if not edge
.is_linked
then return false
205 var url
= edge
.url
.to_s
207 local_edges
[url
] = null
212 # Retrieve all nodes with specified `lbl`
214 # var client = new Neo4jClient("http://neo4j:7474")
216 # var andres = new NeoNode
217 # andres.labels.add_all(["Human", "Male"])
218 # client.save_node(andres)
219 # var kate = new NeoNode
220 # kate.labels.add_all(["Human", "Female"])
221 # client.save_node(kate)
223 # var nodes = client.nodes_with_label("Human")
224 # assert nodes.has(andres)
225 # assert nodes.has(kate)
226 fun nodes_with_label
(lbl
: String): Array[NeoNode] do
227 var res
= get
(base_url
/ "db/data/label/{lbl.to_percent_encoding}/nodes")
228 var nodes
= new Array[NeoNode]
229 for json
in res
.as(JsonArray) do
230 var obj
= json
.as(JsonObject)
231 var node
= load_node
(obj
["self"].to_s
)
232 node
.internal_properties
= obj
["data"].as(JsonObject)
238 # Retrieve nodes belonging to all the specified `labels`.
240 # var client = new Neo4jClient("http://neo4j:7474")
242 # var andres = new NeoNode
243 # andres.labels.add_all(["Human", "Male"])
244 # client.save_node(andres)
245 # var kate = new NeoNode
246 # kate.labels.add_all(["Human", "Female"])
247 # client.save_node(kate)
249 # var nodes = client.nodes_with_labels(["Human", "Male"])
250 # assert nodes.has(andres)
251 # assert not nodes.has(kate)
252 fun nodes_with_labels
(labels
: Array[String]): Array[NeoNode] do
253 assert not labels
.is_empty
256 var buffer
= new Buffer
257 buffer
.append
"match (n) where \{label_0\} in labels(n)"
258 for i
in [1..labels
.length
[ do
259 buffer
.append
" and \{label_{i}\} in labels(n)"
261 buffer
.append
" return n"
262 var query
= new CypherQuery.from_string
(buffer
.write_to_string
)
263 for i
in [0..labels
.length
[ do
264 query
.params
["label_{i}"] = labels
[i
]
267 # Retrieve the answer.
268 var res
= cypher
(query
)
269 var nodes
= new Array[NeoNode]
270 for json
in res
.as(JsonObject)["data"].as(JsonArray) do
271 var obj
= json
.as(JsonArray).first
.as(JsonObject)
272 var node
= load_node
(obj
["self"].to_s
)
273 node
.internal_properties
= obj
["data"].as(JsonObject)
279 # Perform a `CypherQuery`
281 fun cypher
(query
: CypherQuery): Serializable do
282 return post
("{cypher_url}", query
.to_rest
)
285 # GET JSON data from `url`
286 fun get
(url
: String): Serializable do
287 var request
= new JsonGET(url
)
288 var response
= request
.execute
289 return parse_response
(response
)
292 # POST `params` to `url`
293 fun post
(url
: String, params
: Serializable): Serializable do
294 var request
= new JsonPOST(url
)
295 request
.json_data
= params
296 var response
= request
.execute
297 return parse_response
(response
)
300 # PUT `params` at `url`
301 fun put
(url
: String, params
: Serializable): Serializable do
302 var request
= new JsonPUT(url
)
303 request
.json_data
= params
304 var response
= request
.execute
305 return parse_response
(response
)
309 fun delete
(url
: String): Serializable do
310 var request
= new JsonDELETE(url
)
311 var response
= request
.execute
312 return parse_response
(response
)
315 # Parse the cURL `response` as a JSON string
316 private fun parse_response
(response
: CurlResponse): Serializable do
317 if response
isa CurlResponseSuccess then
318 var str
= response
.body_str
319 if str
.is_empty
then return new JsonObject
320 var res
= str
.parse_json
321 if res
isa JsonParseError then
322 var e
= new NeoError(res
.to_s
, "JsonParseError")
327 # empty response wrap it in empty object
328 return new JsonObject
329 else if res
isa JsonObject and res
.has_key
("exception") then
330 var error
= "Neo4jError::{res["exception"] or else "null"}"
332 if res
.has_key
("message") then
333 msg
= res
["message"].to_s
335 return new NeoError(msg
, error
)
339 else if response
isa CurlResponseFailed then
340 return new NeoError("{response.error_msg} ({response.error_code})", "CurlError")
342 return new NeoError("Unexpected response \"{response}\
".", "CurlError")
347 # A Cypher query for Neo4j REST API
349 # The Neo4j REST API allows querying with Cypher.
350 # The results are returned as a list of string headers (columns), and a data part,
351 # consisting of a list of all rows, every row consisting of a list of REST representations
352 # of the field value - Node, Relationship, Path or any simple value like String.
356 # var client = new Neo4jClient("http://neo4j:7474")
357 # var query = new CypherQuery
358 # query.nmatch("(n)-[r:LOVES]->(m)")
359 # query.nwhere("n.name=\"Andres\"")
360 # query.nreturn("m.name")
361 # var res = client.cypher(query).as(JsonObject)
362 # assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
364 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
366 # Query string to perform
367 private var query
: String = ""
369 # `params` to embed in the query like in prepared statements
370 var params
= new JsonObject
372 # init the query from a query string
373 init from_string
(query
: String) do
377 # init the query with parameters
378 init with_params
(params
: JsonObject) do
382 # Pass the argument `value` as the parameter `key`.
385 fun []=(key
: String, value
: nullable Serializable) do
389 # Add a `CREATE` statement to the query
390 fun ncreate
(query
: String): CypherQuery do
391 self.query
= "{self.query}CREATE {query} "
395 # Add a `START` statement to the query
396 fun nstart
(query
: String): CypherQuery do
397 self.query
= "{self.query}START {query} "
401 # Add a `MATCH` statement to the query
402 fun nmatch
(query
: String): CypherQuery do
403 self.query
= "{self.query}MATCH {query} "
407 # Add a `WHERE` statement to the query
408 fun nwhere
(query
: String): CypherQuery do
409 self.query
= "{self.query}WHERE {query} "
413 # Add a `AND` statement to the query
414 fun nand
(query
: String): CypherQuery do
415 self.query
= "{self.query}AND {query} "
419 # Add a `RETURN` statement to the query
420 fun nreturn
(query
: String): CypherQuery do
421 self.query
= "{self.query}RETURN {query} "
425 # Pass the argument `value` as the parameter `key`.
430 # var query = (new CypherQuery).
432 # nwhere("n.key = \{key\}").
435 # assert query.params["key"] == "foo"
439 fun set
(key
: String, value
: nullable Serializable): SELF do
444 # Translate the query to the body of a corresponding Neo4j REST request.
445 fun to_rest
: JsonObject do
446 var obj
= new JsonObject
448 if not params
.is_empty
then
449 obj
["params"] = params
454 redef fun to_s
do return to_rest
.to_s
457 # The fundamental units that form a graph are nodes and relationships.
459 # Entities can have two states:
461 # * linked: the NeoEntity references an existing node or edge in Neo4j
462 # * unlinked: the NeoEntity is not yet created in Neo4j
464 # If the entity is initialized unlinked from neo4j:
466 # # Create a disconnected node
467 # var andres = new NeoNode
468 # andres["name"] = "Andres"
469 # # At this point, the node is not linked
470 # assert not andres.is_linked
472 # Then we can link the entity to the base:
475 # var client = new Neo4jClient("http://neo4j:7474")
476 # client.save_node(andres)
477 # # The node is now linked
478 # assert andres.is_linked
480 # Entities can also be loaded from Neo4j:
482 # # Get a node from Neo4j
483 # var url = andres.url.to_s
484 # var node = client.load_node(url)
485 # assert node.is_linked
487 # When working in connected mode, all reading operations are executed lazily on the base:
489 # # Get the node `name` property
490 # assert node["name"] == "Andres" # loaded lazily from base
491 abstract class NeoEntity
492 # Neo4j client connector
493 private var neo
: Neo4jClient is noinit
495 # Entity unique URL in Neo4j REST API
496 var url
: nullable String = null
498 # Temp id used in batch mode to update the entity
499 private var batch_id
: nullable Int = null
501 # Load the entity from base
502 private init from_neo
(neo
: Neo4jClient, url
: String) is nosuper
do
507 # Init entity from JSON representation
508 private init from_json
(neo
: Neo4jClient, obj
: JsonObject) is nosuper
do
510 self.url
= obj
["self"].to_s
511 self.internal_properties
= obj
["data"].as(JsonObject)
514 # Create a empty (and not-connected) entity
516 self.internal_properties
= new JsonObject
519 # Is the entity linked to a Neo4j database?
520 fun is_linked
: Bool do return url
!= null
522 # In Neo4j, both nodes and relationships can contain properties.
523 # Properties are key-value pairs where the key is a string.
524 # Property values are JSON formatted.
526 # Properties are loaded lazily
527 fun properties
: JsonObject do return internal_properties
or else load_properties
529 private var internal_properties
: nullable JsonObject = null
531 private fun load_properties
: JsonObject do
532 var obj
= neo
.get
(url
.to_s
/ "properties").as(JsonObject)
533 internal_properties
= obj
537 # Get the entity `id` if connected to base
538 fun id
: nullable Int do
539 if url
== null then return null
540 return url
.split
("/").last
.to_i
543 # Get the entity property at `key`
544 fun [](key
: String): nullable Serializable do
545 if not properties
.has_key
(key
) then return null
546 return properties
[key
]
549 # Set the entity property `value` at `key`
550 fun []=(key
: String, value
: nullable Serializable) do properties
[key
] = value
552 # Is the property `key` set?
553 fun has_key
(key
: String): Bool do return properties
.has_key
(key
)
556 # Nodes are used to represent entities stored in base.
557 # Apart from properties and relationships (edges),
558 # nodes can also be labeled with zero or more labels.
560 # A label is a `String` that is used to group nodes into sets.
561 # All nodes labeled with the same label belongs to the same set.
562 # A node may be labeled with any number of labels, including none,
563 # making labels an optional addition to the graph.
565 # Creating new nodes:
567 # var client = new Neo4jClient("http://neo4j:7474")
569 # var andres = new NeoNode
570 # andres.labels.add "Person"
571 # andres["name"] = "Andres"
573 # client.save_node(andres)
574 # assert andres.is_linked
576 # Get nodes from Neo4j:
578 # var url = andres.url.to_s
579 # var node = client.load_node(url)
580 # assert node["name"] == "Andres"
581 # assert node["age"].to_s.to_i == 22
585 private var internal_labels
: nullable Array[String] = null
586 private var internal_in_edges
: nullable List[NeoEdge] = null
587 private var internal_out_edges
: nullable List[NeoEdge] = null
591 self.internal_labels
= new Array[String]
592 self.internal_in_edges
= new List[NeoEdge]
593 self.internal_out_edges
= new List[NeoEdge]
597 var tpl
= new FlatBuffer
599 tpl
.append
"labels: [{labels.join(", ")}],"
600 tpl
.append
"data: {properties.to_json}"
602 return tpl
.write_to_string
605 # A label is a `String` that is used to group nodes into sets.
606 # A node may be labeled with any number of labels, including none.
607 # All nodes labeled with the same label belongs to the same set.
609 # Many database queries can work with these sets instead of the whole graph,
610 # making queries easier to write and more efficient.
612 # Labels are loaded lazily
613 fun labels
: Array[String] do return internal_labels
or else load_labels
615 private fun load_labels
: Array[String] do
616 var labels
= new Array[String]
617 var res
= neo
.get
(url
.to_s
/ "labels")
618 if res
isa JsonArray then
619 for val
in res
do labels
.add val
.to_s
621 internal_labels
= labels
625 # Get the list of `NeoEdge` pointing to `self`
627 # Edges are loaded lazily
628 fun in_edges
: List[NeoEdge] do return internal_in_edges
or else load_in_edges
630 private fun load_in_edges
: List[NeoEdge] do
631 var edges
= new List[NeoEdge]
632 var res
= neo
.get
(url
.to_s
/ "relationships/in").as(JsonArray)
634 edges
.add
(new NeoEdge.from_json
(neo
, obj
.as(JsonObject)))
636 internal_in_edges
= edges
640 # Get the list of `NeoEdge` pointing from `self`
642 # Edges are loaded lazily
643 fun out_edges
: List[NeoEdge] do return internal_out_edges
or else load_out_edges
645 private fun load_out_edges
: List[NeoEdge] do
646 var edges
= new List[NeoEdge]
647 var res
= neo
.get
(url
.to_s
/ "relationships/out")
648 for obj
in res
.as(JsonArray) do
649 edges
.add
(new NeoEdge.from_json
(neo
, obj
.as(JsonObject)))
651 internal_out_edges
= edges
655 # Get nodes pointed by `self` following a `rel_type` edge
656 fun out_nodes
(rel_type
: String): Array[NeoNode] do
657 var res
= new Array[NeoNode]
658 for edge
in out_edges
do
659 if edge
.rel_type
== rel_type
then res
.add edge
.to
664 # Get nodes pointing to `self` following a `rel_type` edge
665 fun in_nodes
(rel_type
: String): Array[NeoNode] do
666 var res
= new Array[NeoNode]
667 for edge
in in_edges
do
668 if edge
.rel_type
== rel_type
then res
.add edge
.from
674 # A relationship between two nodes.
675 # Relationships between nodes are a key part of a graph database.
676 # They allow for finding related data. Just like nodes, relationships can have properties.
678 # Create a relationship:
680 # var client = new Neo4jClient("http://neo4j:7474")
682 # var andres = new NeoNode
683 # andres["name"] = "Andres"
684 # var kate = new NeoNode
685 # kate["name"] = "Kate"
686 # # Create a relationship of type `LOVES`
687 # var loves = new NeoEdge(andres, "LOVES", kate)
688 # client.save_edge(loves)
689 # assert loves.is_linked
691 # Get an edge from DB:
693 # var url = loves.url.to_s
694 # var edge = client.load_edge(url)
695 # assert edge.from["name"].to_s == "Andres"
696 # assert edge.to["name"].to_s == "Kate"
700 private var internal_from
: nullable NeoNode
701 private var internal_to
: nullable NeoNode
702 private var internal_type
: nullable String
703 private var internal_from_url
: nullable String
704 private var internal_to_url
: nullable String
706 init(from
: NeoNode, rel_type
: String, to
: NeoNode) do
707 self.internal_from
= from
708 self.internal_to
= to
709 self.internal_type
= rel_type
712 redef init from_neo
(neo
, url
) do
714 var obj
= neo
.get
(url
).as(JsonObject)
715 self.internal_type
= obj
["type"].to_s
716 self.internal_from_url
= obj
["start"].to_s
717 self.internal_to_url
= obj
["end"].to_s
720 redef init from_json
(neo
, obj
) do
722 self.internal_type
= obj
["type"].to_s
723 self.internal_from_url
= obj
["start"].to_s
724 self.internal_to_url
= obj
["end"].to_s
728 fun from
: NeoNode do return internal_from
or else load_from
730 private fun load_from
: NeoNode do
731 var node
= neo
.load_node
(internal_from_url
.to_s
)
737 fun to
: NeoNode do return internal_to
or else load_to
739 private fun load_to
: NeoNode do
740 var node
= neo
.load_node
(internal_to_url
.to_s
)
746 fun rel_type
: nullable String do return internal_type
748 # Get the JSON body of a REST request that create the relationship.
749 private fun to_rest
: JsonObject do
750 var obj
= new JsonObject
754 obj
["to"] = "\{{to.batch_id.to_s}\}"
756 obj
["type"] = rel_type
757 obj
["data"] = properties
762 # Batches are used to perform multiple operations on the REST API in one cURL request.
763 # This can significantly improve performance for large insert and update operations.
765 # see: http://docs.neo4j.org/chunked/milestone/rest-api-batch-ops.html
767 # This service is transactional.
768 # If any of the operations performed fails (returns a non-2xx HTTP status code),
769 # the transaction will be rolled back and all changes will be undone.
773 # var client = new Neo4jClient("http://neo4j:7474")
775 # var node1 = new NeoNode
776 # var node2 = new NeoNode
777 # var edge = new NeoEdge(node1, "TO", node2)
779 # var batch = new NeoBatch(client)
780 # batch.save_node(node1)
781 # batch.save_node(node2)
782 # batch.save_edge(edge)
785 # assert node1.is_linked
786 # assert node2.is_linked
787 # assert edge.is_linked
790 # Neo4j client connector
791 var client
: Neo4jClient
793 # Jobs to perform in this batch
795 # The batch service expects an array of job descriptions as input,
796 # each job description describing an action to be performed via the normal server API.
797 var jobs
= new HashMap[Int, NeoJob]
799 # Append a new job to the batch in JSON Format
801 fun new_job
(nentity
: NeoEntity): NeoJob do
803 var job
= new NeoJob(id
, nentity
)
808 # Load a node in batch mode also load labels, data and edges
809 fun load_node
(node
: NeoNode) do
810 var job
= new_job
(node
)
811 job
.action
= load_node_data_action
813 if node
.id
!= null then
814 job
.to
= "/node/{node.id.to_s}"
816 job
.to
= "\{{node.batch_id.to_s}\}"
819 job
.action
= load_node_labels_action
821 if node
.id
!= null then
822 job
.to
= "/node/{node.id.to_s}/labels"
824 job
.to
= "\{{node.batch_id.to_s}\}/labels"
828 # Load in and out edges into node
829 fun load_node_edges
(node
: NeoNode) do
830 var job
= new_job
(node
)
831 job
.action
= load_node_in_edges_action
833 if node
.id
!= null then
834 job
.to
= "/node/{node.id.to_s}/relationships/in"
836 job
.to
= "\{{node.batch_id.to_s}\}/relationships/in"
839 job
.action
= load_node_out_edges_action
841 if node
.id
!= null then
842 job
.to
= "/node/{node.id.to_s}/relationships/out"
844 job
.to
= "\{{node.batch_id.to_s}\}/relationships/out"
848 # Create a `NeoNode` or a `NeoEdge` in batch mode.
849 fun save_entity
(nentity
: NeoEntity) do
850 if nentity
isa NeoNode then
852 else if nentity
isa NeoEdge then
857 # Create a node in batch mode also create labels and edges
858 fun save_node
(node
: NeoNode) do
859 if node
.id
!= null or node
.batch_id
!= null then return
861 var job
= new_job
(node
)
862 node
.batch_id
= job
.id
863 job
.action
= create_node_action
866 job
.body
= node
.properties
870 job
.to
= "\{{node.batch_id.to_s}\}/labels"
871 job
.body
= new JsonArray.from
(node
.labels
)
873 #save_edges(node.out_edges)
876 # Create multiple nodes
877 # also create labels and edges
878 fun save_nodes
(nodes
: Collection[NeoNode]) do for node
in nodes
do save_node
(node
)
881 # nodes `edge.from` and `edge.to` will be created if not in base
882 fun save_edge
(edge
: NeoEdge) do
883 if edge
.id
!= null or edge
.batch_id
!= null then return
888 var job
= new_job
(edge
)
889 edge
.batch_id
= job
.id
890 job
.action
= create_edge_action
892 if edge
.from
.id
!= null then
893 job
.to
= "/node/{edge.from.id.to_s}/relationships"
895 job
.to
= "\{{edge.from.batch_id.to_s}\}/relationships"
897 job
.body
= edge
.to_rest
900 # Create multiple edges
901 fun save_edges
(edges
: Collection[NeoEdge]) do for edge
in edges
do save_edge
(edge
)
903 # Execute the batch and update local nodes
904 fun execute
: List[NeoError] do
905 var request
= new JsonPOST(client
.batch_url
)
906 # request.headers["X-Stream"] = "true"
907 var json_jobs
= new JsonArray
908 for job
in jobs
.values
do json_jobs
.add job
.to_rest
909 request
.json_data
= json_jobs
910 var response
= request
.execute
911 var res
= client
.parse_response
(response
)
912 return finalize_batch
(res
)
915 # Associate data from response in original nodes and edges
916 private fun finalize_batch
(response
: Serializable): List[NeoError] do
917 var errors
= new List[NeoError]
918 if not response
isa JsonArray then
919 errors
.add
(new NeoError("Unexpected batch response format.", "Neo4jError"))
922 # print " {res.length} jobs executed"
923 for res
in response
do
924 if not res
isa JsonObject then
925 errors
.add
(new NeoError("Unexpected job format in batch response.", "Neo4jError"))
928 var id
= res
["id"].as(Int)
930 if job
.action
== create_node_action
then
931 var node
= job
.entity
.as(NeoNode)
933 node
.url
= res
["location"].to_s
934 else if job
.action
== create_edge_action
then
935 var edge
= job
.entity
.as(NeoEdge)
937 edge
.url
= res
["location"].to_s
938 else if job
.action
== load_node_data_action
then
939 var node
= job
.entity
.as(NeoNode)
940 node
.internal_properties
= res
["body"].as(JsonObject)["data"].as(JsonObject)
941 else if job
.action
== load_node_labels_action
then
942 var node
= job
.entity
.as(NeoNode)
943 var labels
= new Array[String]
944 for l
in res
["body"].as(JsonArray) do labels
.add l
.to_s
945 node
.internal_labels
= labels
946 else if job
.action
== load_node_in_edges_action
then
947 var node
= job
.entity
.as(NeoNode)
948 var edges
= res
["body"].as(JsonArray)
949 node
.internal_in_edges
= new List[NeoEdge]
951 node
.internal_in_edges
.add client
.load_edge
(edge
.as(JsonObject)["self"].to_s
)
953 else if job
.action
== load_node_out_edges_action
then
954 var node
= job
.entity
.as(NeoNode)
955 var edges
= res
["body"].as(JsonArray)
956 node
.internal_out_edges
= new List[NeoEdge]
958 node
.internal_out_edges
.add client
.load_edge
(edge
.as(JsonObject)["self"].to_s
)
966 # TODO replace with enum
968 private fun create_node_action
: Int do return 1
969 private fun create_edge_action
: Int do return 2
970 private fun load_node_data_action
: Int do return 3
971 private fun load_node_labels_action
: Int do return 4
972 private fun load_node_in_edges_action
: Int do return 5
973 private fun load_node_out_edges_action
: Int do return 6
976 # A job that can be executed in a `NeoBatch`
977 # This is a representation of a neo job in JSON Format
979 # Each job description should contain a `to` attribute, with a value relative to the data API root
980 # (so http://neo4j:7474/db/data/node becomes just /node), and a `method` attribute containing
983 # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track
984 # of responses, although responses are guaranteed to be returned in the same order the job
985 # descriptions are received.
989 # Entity targeted by the job
990 var entity
: NeoEntity
992 init(id
: Int, entity
: NeoEntity) do
997 # What kind of action do the job
998 # used to attach responses to original Neo objets
999 private var action
: nullable Int = null
1001 # Job HTTP method: `GET`, `POST`, `PUT`, `DELETE`...
1003 # Job service target: `/node`, `/labels` etc...
1005 # Body to send with the job service request
1006 var body
: nullable Serializable = null
1009 fun to_rest
: JsonObject do
1010 var job
= new JsonObject
1012 job
["method"] = method
1014 if not body
== null then