Ok, here is something quick-and-dirty I put together to give you an example of the most important concepts: finding a node, creating a node, and creating a relationship between two nodes. I returned the created nodes so you can see how to return data and so it would display the results if you executed it in Neo4j desktop.
I set the 'v' property of the last node in the list to the passed timestamp.
This is not optimized, nor probably how I would do it in production. I would probably look at a recursive algorithm to compact the code.
package customProcedures;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.logging.Log;
import org.neo4j.procedure.*;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
public class MergeListOfNodesProcedure {
@Context
public Log log;
@Context
public Transaction tx;
@Procedure(name = "custom.mergeList", mode = Mode.WRITE)
@Description("Merge and link a list of nodes specified by their parameter 'l'")
public Stream<MergeResult> mergeList(@Name("nodeLabel") String labelString, @Name("list") List<String> nodeIds, @Name("timestamp") String timestamp) {
Objects.requireNonNull(nodeIds);
Objects.requireNonNull(timestamp);
Objects.requireNonNull(labelString);
if (!nodeIds.isEmpty()) {
Label label = Label.label(labelString);
List<Node> results = processList(label, nodeIds, timestamp);
return results.stream().map(MergeResult::of);
} else {
return Stream.empty();
}
}
private List<Node> processList(Label label, List<String> nodeIds, String timestamp) {
String rootNodeId = nodeIds.get(0);
Node rootNode = mergeRootNode(label, rootNodeId);
List<String> otherNodeIds = nodeIds.stream().skip(1).toList();
if (!otherNodeIds.isEmpty()) {
List<Node> nodes = new ArrayList<>();
nodes.add(rootNode);
Node parentNode = rootNode;
for (String childNodeId : otherNodeIds) {
Node childNode = mergeChildNode(label, parentNode, childNodeId);
nodes.add(childNode);
parentNode = childNode;
}
parentNode.setProperty("v", ZonedDateTime.parse(timestamp));
return nodes;
} else {
return Collections.singletonList(rootNode);
}
}
private Node mergeRootNode(Label label, String nodeId) {
Node rootNode = tx.findNode(label, "l", nodeId);
if (rootNode == null) {
rootNode = tx.createNode(label);
rootNode.setProperty("l", nodeId);
}
return rootNode;
}
private Node mergeChildNode(Label label, Node parentNode, String childNodeId) {
Node childNode = tx.findNode(label, "l", childNodeId);
if (childNode == null) {
childNode = tx.createNode(label);
childNode.setProperty("l", childNodeId);
}
parentNode.createRelationshipTo(childNode, RelationshipType.withName("N"));
return childNode;
}
public static class MergeResult {
public Node node;
private MergeResult(Node nodes) {
this.node = nodes;
}
public static MergeResult of(Node nodes) {
return new MergeResult(nodes);
}
}
}
The custom procedure processes one list of nodes. The way to process your list of lists is to unwind the list of lists into rows of a single list and call the custom procedure for each list. Here is an example.
with [
["a1", "b1", "c1", "2020-01-01T10:00:00-05:00"],
["a2", "b2", "c2", "2022-01-01T10:00:00-05:00"],
["a3", "b3", "c3", "2023-01-01T10:00:00-05:00"]
] as lists
unwind lists as list
with list[..3] as ids, list[3] as timestamp
call custom.mergeList("D", ids, timestamp) yield node
return node
You can modify the behavior of the custom procedure to take the entire list of lists and process it at once. It is up to you. I do like the approach I took though, as it is more flexible.
I created two unit tests to test it worked, as well as give you an example of how to setup the test harness.
package customProcedures;
import org.junit.jupiter.api.*;
import org.neo4j.driver.*;
import org.neo4j.driver.types.Node;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
class MergeListOfNodesProcedureTest {
static Driver driver;
static Neo4j neo4j;
@BeforeAll
static void setup_db() {
neo4j = Neo4jBuilders.newInProcessBuilder()
.withProcedure(MergeListOfNodesProcedure.class)
.build();
driver = GraphDatabase.driver(neo4j.boltURI(), Config.builder()
.withoutEncryption()
.build());
}
@AfterAll
static void tear_down() {
driver.close();
neo4j.close();
}
@BeforeEach
void delete_data() {
try (Session session = driver.session()) {
session.run("match(n) detach delete n");
}
}
@Test
void test_list_of_three_non_existent_nodes() {
String cypher = """
call custom.mergeList("D", ["a", "b", "c"], "1970-01-01T10:00:00-05:00") yield node
return node""";
List<Node> nodes = getCypherResults(cypher);
assertEquals(3, nodes.size());
assertTrue(nodes.stream().allMatch(x->x.hasLabel("D")));
assertIterableEquals(List.of("a", "b", "c"), nodes.stream().map(v -> v.get("l").asString()).sorted().collect(Collectors.toList()));
assertTrue(nodes.stream().filter(x->"c".equals(x.get("l").asString())).allMatch(y->ZonedDateTime.parse("1970-01-01T10:00:00-05:00").equals(y.get("v").asZonedDateTime())));
}
@Test
void test_list_of_three_nodes_with_first_two_existing() {
String cypher = """
create(n1:D{l:"x"})
create(n2:D{l:"y"})
create(n1)-[:N]->(n2)
with n1, n2
call custom.mergeList("D", ["x", "y", "z"], "2020-11-01T10:24:15-05:00") yield node
return node""";
List<Node> nodes = getCypherResults(cypher);
assertEquals(3, nodes.size());
assertTrue(nodes.stream().allMatch(x->x.hasLabel("D")));
assertIterableEquals(List.of("x", "y", "z"), nodes.stream().map(v -> v.get("l").asString()).sorted().collect(Collectors.toList()));
assertTrue(nodes.stream().filter(x->"c".equals(x.get("l").asString())).allMatch(y->ZonedDateTime.parse("2020-11-01T10:24:15-05:00").equals(y.get("v").asZonedDateTime())));
}
private List<Node> getCypherResults(String cypher) {
try (Session session = driver.session()) {
Result result = session.run(cypher);
return result.list(x -> x.get("node").asNode());
}
}
}
I can send you the IntelliJ project via email if you want it.