diff --git a/README.md b/README.md index 3679c2f..bd23940 100644 --- a/README.md +++ b/README.md @@ -51,4 +51,4 @@ We would appreciate your feedback about our Versioner Core, how to improve and f ## Buy us a coffee :coffee: -This project is developed during our free time, and our free time is mostly during evening/night! So coffee is really helpful during our sessions :sweat_smile:. If you want to help us with that, you can buy us some :coffee: thanks to [PayPal](https://www.paypal.me/mfalcier/2)! +This project is developed during our free time, and our free time is mostly during evening/night! So coffee is really helpful during our sessions :sweat_smile:. If you want to help us with that, you can buy us some :coffee: thanks to [PayPal](https://www.paypal.me/mfalcier/2)! \ No newline at end of file diff --git a/build.gradle b/build.gradle index 921ccfc..116e5bf 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'java' group = 'org.homer' -version = '2.0.0' +version = '2.0.2' description = """Neo4j Procedures for Graph Versioning""" diff --git a/docs/index.md b/docs/index.md index b1a7061..c35ffab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,8 +116,11 @@ name | parameters | return values | description [graph.versioner.diff](#diff) | **stateFrom**, **stateTo** | operation, label, oldValue, newValue | Get a list of differences that must be applied to stateFrom in order to convert it into stateTo. [graph.versioner.diff.from.previous](#diff-from-previous) | **state** | operation, label, oldValue, newValue | Get a list of differences that must be applied to the previous status of the given one in order to become the given state. [graph.versioner.diff.from.current](#diff-from-current) | **state** | operation, label, oldValue, newValue | Get a list of differences that must be applied to the given state in order to become the current entity state. +[graph.versioner.relationships.createTo](#relationships-createTo) | **entitySource**, **List**, relationshipType, *{key:value,...}*, *date* | **relationship** | Creates a new state for the source entity connected to each of the R nodes of the destinations with a relationship of the given type. +[graph.versioner.relationships.createFrom](#relationships-createFrom) | **List**, **entityDestination**, relationshipType, *{key:value,...}*, *date* | **relationship** | Creates a new state for each of the source entities connected to the R nodes of the destination with a relationship of the given type. [graph.versioner.relationship.create](#relationship-create) | **entitySource**, **entityDestination**, relationshipType, *{key:value,...}*, *date* | **relationship** | Creates a new state for the source entity connected to the R node of the destination with a relationship of the given type. [graph.versioner.relationship.delete](#relationship-delete) | **entitySource**, **entityDestination**, relationshipType, *date* | **result** | Creates a new state for the source entity without a custom relationship of the given type. +[graph.versioner.relationships.delete](#relationships-delete) | **entitySource**, **List**, relationshipType, *date* | **result** | Creates a new state for the source entity without a custom relationships for each of the destination nodes of the given type. ## init diff --git a/src/main/java/org/homer/versioner/core/Utility.java b/src/main/java/org/homer/versioner/core/Utility.java index 077633b..3758faf 100644 --- a/src/main/java/org/homer/versioner/core/Utility.java +++ b/src/main/java/org/homer/versioner/core/Utility.java @@ -1,9 +1,9 @@ package org.homer.versioner.core; +import org.apache.commons.lang3.tuple.Pair; import org.homer.versioner.core.exception.VersionerCoreException; import org.homer.versioner.core.output.NodeOutput; import org.homer.versioner.core.output.RelationshipOutput; -import org.homer.versioner.core.procedure.RelationshipProcedure; import org.neo4j.graphdb.*; import java.time.Instant; @@ -11,6 +11,7 @@ import java.time.ZoneId; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -143,10 +144,10 @@ public static void addCurrentState(Node state, Node entity, LocalDateTime instan * @param state a {@link Node} representing the State * @return {@link Boolean} result */ - public static Boolean checkRelationship(Node entity, Node state) { + public static void checkRelationship(Node entity, Node state) throws VersionerCoreException { Spliterator stateRelIterator = state.getRelationships(RelationshipType.withName(Utility.HAS_STATE_TYPE), Direction.INCOMING).spliterator(); - Boolean check = StreamSupport.stream(stateRelIterator, false).map(hasStateRel -> { + StreamSupport.stream(stateRelIterator, false).map(hasStateRel -> { Node maybeEntity = hasStateRel.getStartNode(); if (maybeEntity.getId() != entity.getId()) { throw new VersionerCoreException("Can't patch the given entity, because the given State is owned by another entity."); @@ -156,7 +157,6 @@ public static Boolean checkRelationship(Node entity, Node state) { throw new VersionerCoreException("Can't find any entity node relate to the given State."); }); - return check; } /** @@ -195,9 +195,9 @@ public static LocalDateTime defaultToNow(LocalDateTime date) { * @param node the {@link Node} to check * @throws VersionerCoreException */ - public static void isEntityOrThrowException(Node node) throws VersionerCoreException { + public static void isEntityOrThrowException(Node node) { - streamOfIterable(node.getRelationships(RelationshipType.withName("CURRENT"), Direction.OUTGOING)).findAny() + streamOfIterable(node.getRelationships(RelationshipType.withName(CURRENT_TYPE), Direction.OUTGOING)).findAny() .map(ignored -> streamOfIterable(node.getRelationships(RelationshipType.withName("R"), Direction.INCOMING)).findAny()) .orElseThrow(() -> new VersionerCoreException("The given node is not a Versioner Core Entity")); } @@ -218,11 +218,22 @@ public static Optional getCurrentRelationship(Node entity) { .findFirst(); } + public static Optional getCurrentState(Node entity) { + return streamOfIterable(entity.getRelationships(RelationshipType.withName(CURRENT_TYPE), Direction.OUTGOING)).map(relationship -> relationship.getEndNode()).findFirst(); + } + public static LocalDateTime convertEpochToLocalDateTime(Long epochDateTime) { return Instant.ofEpochMilli(epochDateTime).atZone(ZoneId.systemDefault()).toLocalDateTime(); } - public static Boolean isSystemType(String type) { + public static List> zip(List listA, List listB) { + return IntStream.range(0, listA.size()) + .mapToObj(i -> Pair.of(listA.get(i), listB.get(i))) + .collect(Collectors.toList()); + + } + + public static boolean isSystemType(String type) { return SYSTEM_RELS.contains(type); } } diff --git a/src/main/java/org/homer/versioner/core/builders/CoreProcedureBuilder.java b/src/main/java/org/homer/versioner/core/builders/CoreProcedureBuilder.java index 7c23207..b185d94 100644 --- a/src/main/java/org/homer/versioner/core/builders/CoreProcedureBuilder.java +++ b/src/main/java/org/homer/versioner/core/builders/CoreProcedureBuilder.java @@ -4,6 +4,7 @@ import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.logging.Log; +import java.lang.reflect.InvocationTargetException; import java.util.Optional; /** @@ -13,7 +14,7 @@ */ public abstract class CoreProcedureBuilder { - private Class clazz; + private final Class clazz; private GraphDatabaseService db; private Log log; @@ -62,13 +63,12 @@ public CoreProcedureBuilder withLog(Log log) { * @return instance */ Optional instantiate() { - T instance; + T instance = null; try { - instance = clazz.newInstance(); + instance = clazz.getDeclaredConstructor().newInstance(); instance.db = db; instance.log = log; - } catch (InstantiationException | IllegalAccessException e) { - instance = null; + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e ) { log.error(e.getMessage()); } return Optional.ofNullable(instance); diff --git a/src/main/java/org/homer/versioner/core/procedure/Get.java b/src/main/java/org/homer/versioner/core/procedure/Get.java index acacaad..2a07601 100644 --- a/src/main/java/org/homer/versioner/core/procedure/Get.java +++ b/src/main/java/org/homer/versioner/core/procedure/Get.java @@ -3,6 +3,7 @@ import org.homer.versioner.core.Utility; import org.homer.versioner.core.output.NodeOutput; import org.homer.versioner.core.output.PathOutput; +import org.neo4j.cypher.internal.compiler.v2_3.commands.expressions.PathValueBuilder; import org.neo4j.graphalgo.impl.util.PathImpl; import org.neo4j.graphdb.*; import org.neo4j.procedure.Description; @@ -44,22 +45,23 @@ public Stream getCurrentState( .map(Relationship::getEndNode).map(NodeOutput::new).orElse(null)); } + + //fix for error Node[xyz] not connected to this relationship[xyz] @Procedure(value = "graph.versioner.get.all", mode = DEFAULT) @Description("graph.versioner.get.all(entity) - Get all the State nodes for the given Entity.") public Stream getAllState( @Name("entity") Node entity) { - PathImpl.Builder builder = new PathImpl.Builder(entity) - .push(entity.getSingleRelationship(RelationshipType.withName(Utility.CURRENT_TYPE), Direction.OUTGOING)); - builder = StreamSupport.stream(entity.getRelationships(RelationshipType.withName(Utility.HAS_STATE_TYPE), Direction.OUTGOING).spliterator(), false) - //.sorted((a, b) -> -1 * Long.compare((long)a.getProperty(START_DATE_PROP), (long)b.getProperty(START_DATE_PROP))) - .reduce( - builder, - (build, rel) -> Optional.ofNullable(rel.getEndNode().getSingleRelationship(RelationshipType.withName(Utility.PREVIOUS_TYPE), Direction.OUTGOING)) - .map(build::push) - .orElse(build), - (a, b) -> a); - return Stream.of(new PathOutput(builder.build())); + PathValueBuilder builder = new PathValueBuilder(); + builder.addNode(entity); + builder.addOutgoingRelationship(entity.getSingleRelationship(RelationshipType.withName(Utility.CURRENT_TYPE), Direction.OUTGOING)); + StreamSupport.stream(entity.getRelationships(RelationshipType.withName(Utility.HAS_STATE_TYPE), Direction.OUTGOING).spliterator(), false) + .forEach(rel -> + Optional.ofNullable(rel.getEndNode().getSingleRelationship(RelationshipType.withName(Utility.PREVIOUS_TYPE), Direction.OUTGOING)) + .map(builder::addOutgoingRelationship) + ); + + return Stream.of(new PathOutput(builder.result())); } @Procedure(value = "graph.versioner.get.by.label", mode = DEFAULT) @@ -86,31 +88,31 @@ public Stream getStateByDate( .map(NodeOutput::new); } - @Procedure(value = "graph.versioner.get.nth.state", mode = DEFAULT) - @Description("graph.versioner.get.nth.state(entity, nth) - Get the nth State node for the given Entity.") - public Stream getNthState( - @Name("entity") Node entity, - @Name("nth") long nth) { - - return getCurrentState(entity) - .findFirst() - .flatMap(currentState -> getNthStateFrom(currentState.node, nth)) - .map(Utility::streamOfNodes) - .orElse(Stream.empty()); - } - - private Optional getNthStateFrom(Node state, long nth) { - - return Stream.iterate(Optional.of(state), s -> s.flatMap(this::jumpToPreviousState)) - .limit(nth + 1) - .reduce((a, b) -> b) //get only the last value (apply jumpToPreviousState n times - .orElse(Optional.empty()); - } - - private Optional jumpToPreviousState(Node state) { - - return StreamSupport.stream(state.getRelationships(RelationshipType.withName(Utility.PREVIOUS_TYPE), Direction.OUTGOING).spliterator(), false) - .findFirst() - .map(Relationship::getEndNode); - } + @Procedure(value = "graph.versioner.get.nth.state", mode = DEFAULT) + @Description("graph.versioner.get.nth.state(entity, nth) - Get the nth State node for the given Entity.") + public Stream getNthState( + @Name("entity") Node entity, + @Name("nth") long nth) { + + return getCurrentState(entity) + .findFirst() + .flatMap(currentState -> getNthStateFrom(currentState.node, nth)) + .map(Utility::streamOfNodes) + .orElse(Stream.empty()); + } + + private Optional getNthStateFrom(Node state, long nth) { + + return Stream.iterate(Optional.of(state), s -> s.flatMap(this::jumpToPreviousState)) + .limit(nth + 1) + .reduce((a, b) -> b) //get only the last value (apply jumpToPreviousState n times + .orElse(Optional.empty()); + } + + private Optional jumpToPreviousState(Node state) { + + return StreamSupport.stream(state.getRelationships(RelationshipType.withName(Utility.PREVIOUS_TYPE), Direction.OUTGOING).spliterator(), false) + .findFirst() + .map(Relationship::getEndNode); + } } diff --git a/src/main/java/org/homer/versioner/core/procedure/RelationshipProcedure.java b/src/main/java/org/homer/versioner/core/procedure/RelationshipProcedure.java index e818066..892041b 100644 --- a/src/main/java/org/homer/versioner/core/procedure/RelationshipProcedure.java +++ b/src/main/java/org/homer/versioner/core/procedure/RelationshipProcedure.java @@ -16,10 +16,10 @@ import org.neo4j.procedure.Procedure; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static org.homer.versioner.core.Utility.*; @@ -27,6 +27,78 @@ * RelationshipProcedure class, it contains all the Procedures needed to create versioned relationships between Entities */ public class RelationshipProcedure extends CoreProcedure { + @Procedure(value = "graph.versioner.relationships.createTo", mode = Mode.WRITE) + @Description("graph.versioner.relationships.create(entityA, entitiesB, relProps, date) - Create multiple relationships from entitySource to each of the entityDestinations with the given type and/or properties for the specified date. The relationship 'versionerLabel' along with properties for each relationship can be passed in via 'relProps' a default label ('LABEL_UNDEFINED') is assigned to relationships that are not supplied with a 'versionerLabel' attribute in the props") + public Stream relationshipsCreate( + @Name("entitySource") Node entitySource, + @Name("entityDestinations") List entityDestinations, + @Name(value = "relProps", defaultValue = "[{}]") List> relProps, + @Name(value = "date", defaultValue = "null") LocalDateTime date) { + + Optional sourceCurrentState = createNewSourceState(entitySource, defaultToNow(date)); + isEntityOrThrowException(entitySource); + entityDestinations.sort(Comparator.comparing(o -> o.getProperty("id").toString())); + + Stream relationshipOutputStream = getRelationshipOutputStream(entityDestinations, relProps, sourceCurrentState); + return relationshipOutputStream; + } + + @Procedure(value = "graph.versioner.relationships.createFrom", mode = Mode.WRITE) + @Description("graph.versioner.relationships.createfrom(entitiesA, entityB, relProps, date) - Create multiple relationships from each of the entitySources to the entityDestination with the given type and/or properties for the specified date. The relationship 'versionerLabel' along with properties for each relationship can be passed in via 'relProps' a default label ('LABEL_UNDEFINED') is assigned to relationships that are not supplied with a 'versionerLabel' attribute in the props") + public Stream relationshipsCreateFrom( + @Name("entitySources") List entitySources, + @Name("entityDestination") Node entityDestination, + @Name(value = "relProps", defaultValue = "[{}]") List> relProps, + @Name(value = "date", defaultValue = "null") LocalDateTime date) { + + entitySources.sort(Comparator.comparing(o -> o.getProperty("id").toString())); + entitySources.stream().map(node -> { + log.info("orderedNodes: " + node.getProperty("id").toString()); + return null; + }); + Stream out = null; + Optional destinationRNode = getRNode(entityDestination); + isEntityOrThrowException(entityDestination); + if (entitySources.size() == relProps.size() && destinationRNode.isPresent()) { + out = zip(entitySources, relProps).stream().map((item) -> { + final Node entitySource = item.getLeft(); + log.info("entity source: " + entitySource.getProperty("id").toString()); + Map props = item.getRight(); + final String labelProp = "versionerLabel"; + Map filteredProps = props.entrySet().stream().filter(map -> !map.getKey().equals(labelProp)) + .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); + String type = props.get(labelProp) instanceof String ? props.get(labelProp).toString() : "LABEL_UNDEFINED"; + Optional sourceCurrentState = createNewSourceState(entitySource, defaultToNow(date)); + boolean exists = StreamSupport.stream(sourceCurrentState.get().getRelationships(Direction.OUTGOING, RelationshipType.withName(type)).spliterator(), false).anyMatch(relationship -> relationship.getEndNode().getId() == destinationRNode.get().getId()); + if (exists) { + return Stream.empty(); + } else { + log.info("creating relationship from: " + sourceCurrentState.get().getId() + " to: " + destinationRNode.get().getId()); + return streamOfRelationships(createRelationship(sourceCurrentState.get(), destinationRNode.get(), type, filteredProps)); + } + }).reduce(Stream::concat).orElseGet(Stream::empty); + } + return out; + } + + private Stream getRelationshipOutputStream(List entityDestinations, List> relProps, Optional sourceCurrentState) { + return entityDestinations.size() == relProps.size() ? sourceCurrentState.map(node -> zip(entityDestinations, relProps).stream().map((item) -> { + final Node destinationNode = item.getLeft(); + isEntityOrThrowException(destinationNode); + Optional destinationRNode = getRNode(destinationNode); + Map props = item.getRight(); + final String labelProp = "versionerLabel"; + Map filteredProps = props.entrySet().stream().filter(map -> !map.getKey().equals(labelProp)) + .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); + if (destinationRNode.isPresent()) { + String type = props.get(labelProp) instanceof String ? props.get(labelProp).toString() : "LABEL_UNDEFINED"; + streamOfRelationships(createRelationship(node, destinationRNode.get(), type, filteredProps)); + } + return Stream.empty(); + + }).reduce(Stream::concat).orElseGet(Stream::empty)).orElseGet(Stream::empty) : Stream.empty(); + } + @Procedure(value = "graph.versioner.relationship.create", mode = Mode.WRITE) @Description("graph.versioner.relationship.create(entityA, entityB, type, relProps, date) - Create a relationship from entitySource to entityDestination with the given type and/or properties for the specified date.") @@ -43,6 +115,10 @@ public Stream relationshipCreate( Optional sourceCurrentState = createNewSourceState(entitySource, defaultToNow(date)); Optional destinationRNode = getRNode(entityDestination); + boolean exists = StreamSupport.stream(sourceCurrentState.get().getRelationships(Direction.OUTGOING, RelationshipType.withName(type)).spliterator(), true).anyMatch(relationship -> relationship.getEndNode().getId() == destinationRNode.get().getId()); + if (exists) { + return null; + } if (sourceCurrentState.isPresent() && destinationRNode.isPresent()) { return streamOfRelationships(createRelationship(sourceCurrentState.get(), destinationRNode.get(), type, relProps)); } else { @@ -50,34 +126,58 @@ public Stream relationshipCreate( } } - @Procedure(value = "graph.versioner.relationship.delete", mode = Mode.WRITE) - @Description("graph.versioner.relationship.delete(entityA, entityB, type, date) - Delete a custom type relationship from entitySource's current State to entityDestination for the specified date.") - public Stream relationshipDelete( + @Procedure(value = "graph.versioner.relationships.delete", mode = Mode.WRITE) + @Description("graph.versioner.relationship.delete(entityA, entityB, type, date) - Delete multiple custom type relationship from entitySource's current State to each of the entityDestinations for the specified date.") + public Stream relationshipsDelete( @Name("entitySource") Node entitySource, - @Name("entityDestination") Node entityDestination, + @Name("entityDestinations") List entityDestinations, @Name(value = "type") String type, @Name(value = "date", defaultValue = "null") LocalDateTime date) { - isEntityOrThrowException(entitySource); - isEntityOrThrowException(entityDestination); - if (isSystemType(type)) { - throw new VersionerCoreException("It's not possible to delete a System Relationship like " + type + "."); - } - Optional sourceCurrentState = createNewSourceState(entitySource, defaultToNow(date)); - Optional destinationRNode = getRNode(entityDestination); + Optional sourceCurrentState = getCurrentState(entitySource); + entityDestinations.stream().map((entityDestination) -> { + return getBooleanOutputStream(entitySource, type, sourceCurrentState, entityDestination); + }); + return Stream.of(new BooleanOutput(Boolean.FALSE)); + } + private Stream getBooleanOutputStream(@Name("entitySource") Node entitySource, @Name("type") String type, Optional sourceCurrentState, Node entityDestination) { + Optional destinationRNode = getRNode(entityDestination); Update updateProcedure = new UpdateBuilder().withLog(log).withDb(db).build().orElseThrow(() -> new VersionerCoreException("Unable to initialize update procedure")); if (sourceCurrentState.isPresent() && destinationRNode.isPresent()) { + final long destId = destinationRNode.get().getId(); updateProcedure.update(entitySource, sourceCurrentState.get().getAllProperties(), "", null); - getCurrentRelationship(entitySource).ifPresent(rel -> rel.getEndNode().getRelationships(RelationshipType.withName(type), Direction.OUTGOING).forEach(Relationship::delete)); + getCurrentRelationship(entitySource).ifPresent(rel -> rel.getEndNode().getRelationships(RelationshipType.withName(type), Direction.OUTGOING).forEach(rel2 -> { + if (rel2.getEndNode().getId() == destId) { + rel2.delete(); + } + })); return Stream.of(new BooleanOutput(Boolean.TRUE)); } else { return Stream.of(new BooleanOutput(Boolean.FALSE)); } } + @Procedure(value = "graph.versioner.relationship.delete", mode = Mode.WRITE) + @Description("graph.versioner.relationship.delete(entityA, entityB, type, date) - Delete a custom type relationship from entitySource's current State to entityDestination for the specified date.") + public Stream relationshipDelete( + @Name("entitySource") Node entitySource, + @Name("entityDestination") Node entityDestination, + @Name(value = "type") String type, + @Name(value = "date", defaultValue = "null") LocalDateTime date) { + + isEntityOrThrowException(entitySource); + isEntityOrThrowException(entityDestination); + if (isSystemType(type)) { + throw new VersionerCoreException("It's not possible to delete a System Relationship like " + type + "."); + } + + Optional sourceCurrentState = getCurrentState(entitySource); + return getBooleanOutputStream(entitySource, type, sourceCurrentState, entityDestination); + } + static Relationship createRelationship(Node source, Node destination, String type, Map relProps) { Relationship rel = source.createRelationshipTo(destination, RelationshipType.withName(type)); @@ -90,7 +190,7 @@ private Optional getRNode(Node entity) { .map(Relationship::getStartNode); } - private Optional createNewSourceState(Node entitySource, LocalDateTime date) throws VersionerCoreException { + private Optional createNewSourceState(Node entitySource, LocalDateTime date) { Update updateProcedure = new UpdateBuilder().withLog(log).withDb(db).build().orElseThrow(() -> new VersionerCoreException("Unable to initialize update procedure")); return updateProcedure.patch(entitySource, Collections.emptyMap(), StringUtils.EMPTY, date) diff --git a/src/main/java/org/homer/versioner/core/procedure/Update.java b/src/main/java/org/homer/versioner/core/procedure/Update.java index 77a0c91..5140eec 100644 --- a/src/main/java/org/homer/versioner/core/procedure/Update.java +++ b/src/main/java/org/homer/versioner/core/procedure/Update.java @@ -22,6 +22,9 @@ */ public class Update extends CoreProcedure { + public static final String DATE_FIELD = "date"; + public static final String R_LABEL = "R"; + @Procedure(value = "graph.versioner.update", mode = Mode.WRITE) @Description("graph.versioner.update(entity, {key:value,...}, additionalLabel, date) - Add a new State to the given Entity.") public Stream update( @@ -44,7 +47,7 @@ public Stream update( StreamSupport.stream(currentRelIterator, false).forEach(currentRel -> { Node currentState = currentRel.getEndNode(); - LocalDateTime currentDate = (LocalDateTime) currentRel.getProperty("date"); + LocalDateTime currentDate = (LocalDateTime) currentRel.getProperty(DATE_FIELD); // Creating PREVIOUS relationship between the current and the new State result.createRelationshipTo(currentState, RelationshipType.withName(PREVIOUS_TYPE)).setProperty(DATE_PROP, currentDate); @@ -117,7 +120,7 @@ public Stream patchFrom( .orElseThrow(() -> new VersionerCoreException("Can't find any current State node for the given entity.")); //Copy all the relationships - if (useCurrentRel) { + if (Boolean.TRUE.equals(useCurrentRel)) { currentRelationshipOpt.ifPresent(rel -> connectStateToRs(rel.getEndNode() , newState)); } else { connectStateToRs(state, newState); @@ -131,7 +134,7 @@ public Stream patchFrom( private Node createPatchedState(Map stateProps, List labels, LocalDateTime instantDate, Relationship currentRelationship) { Node currentState = currentRelationship.getEndNode(); - LocalDateTime currentDate = (LocalDateTime) currentRelationship.getProperty("date"); + LocalDateTime currentDate = (LocalDateTime) currentRelationship.getProperty(DATE_FIELD); Node entity = currentRelationship.getStartNode(); // Patching the current node into the new one. @@ -145,7 +148,7 @@ private Node createPatchedState(Map stateProps, List lab protected static void connectStateToRs(Node sourceState, Node newState) { streamOfIterable(sourceState.getRelationships(Direction.OUTGOING)) - .filter(rel -> rel.getEndNode().hasLabel(Label.label("R"))) + .filter(rel -> rel.getEndNode().hasLabel(Label.label(R_LABEL))) .forEach(rel -> RelationshipProcedure.createRelationship(newState, rel.getEndNode(), rel.getType().name(), rel.getAllProperties())); } } diff --git a/src/test/java/org/homer/versioner/core/procedure/GetTest.java b/src/test/java/org/homer/versioner/core/procedure/GetTest.java index 6f6768e..4561565 100644 --- a/src/test/java/org/homer/versioner/core/procedure/GetTest.java +++ b/src/test/java/org/homer/versioner/core/procedure/GetTest.java @@ -1,5 +1,6 @@ package org.homer.versioner.core.procedure; +import org.hamcrest.CoreMatchers; import org.homer.versioner.core.Utility; import org.junit.Rule; import org.junit.Test; @@ -9,6 +10,7 @@ import org.neo4j.driver.v1.types.Relationship; import org.neo4j.harness.junit.Neo4jRule; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -20,12 +22,12 @@ /** * GetTest class, it contains all the method used to test Get class methods */ -public class GetTest { +public class GetTest extends GenericProcedureTest{ @Rule public Neo4jRule neo4j = new Neo4jRule() // This is the function we want to test - .withProcedure(Get.class); + .withProcedure(Get.class).withProcedure(Update.class); /*------------------------------*/ /* get.current.path */ @@ -118,6 +120,32 @@ public void shouldGetAllStateNodesByGivenEntity() { } } + @Test + public void shouldGetAllStateNodesByGivenEntityMultipleStates() { + // This is in a try-block, to make sure we close the driver after the test + try (Driver driver = GraphDatabase + .driver(neo4j.boltURI(), Config.build().withEncryption().toConfig()); Session session = driver.session()) { + // Given + session.run("CREATE (e:Entity {key:'immutableValue'})-[:CURRENT {date:localdatetime('1988-10-27T00:00:00')}]->(s:State {key:'initialValue'})"); + session.run("MATCH (e:Entity)-[:CURRENT]->(s:State) CREATE (e)-[:HAS_STATE {startDate:localdatetime('1988-10-27T00:00:00')}]->(s)"); + + // When + session.run("MATCH (e:Entity) WITH e CALL graph.versioner.update(e, {key:'newValue'}, 'Error') YIELD node RETURN node"); + session.run("MATCH (e:Entity) WITH e CALL graph.versioner.update(e, {key:'newerValue'}, 'Error') YIELD node RETURN node"); + session.run("MATCH (e:Entity) WITH e CALL graph.versioner.update(e, {key:'newestValue'}, 'Error') YIELD node RETURN node"); + StatementResult countStateResult = session.run("MATCH (s:State) RETURN count(*) as ss"); + StatementResult allResult = session.run("MATCH (e:Entity) WITH e CALL graph.versioner.get.all(e) YIELD path RETURN nodes(path) as ns"); + StatementResult allResult2 = session.run("MATCH (e:Entity) WITH e CALL graph.versioner.get.all(e) YIELD path RETURN path"); + // Then + final Value ss = countStateResult.single().get("ss"); + assertThat(ss.asInt(), CoreMatchers.equalTo(4)); + final Value ns = allResult.single().get("ns"); + assertThat(ns.asList().size(), CoreMatchers.equalTo(5)); + final Iterable path = allResult2.single().get("path").asPath().nodes(); + assertThat(((Collection)path).size(), CoreMatchers.equalTo(5)); + } + } + @Test public void shouldGetAllStateNodesByGivenEntityWithOnlyOneCurrentState() { // This is in a try-block, to make sure we close the driver after the test diff --git a/src/test/java/org/homer/versioner/core/procedure/RelationshipProcedureTest.java b/src/test/java/org/homer/versioner/core/procedure/RelationshipProcedureTest.java index c88650d..5f71670 100644 --- a/src/test/java/org/homer/versioner/core/procedure/RelationshipProcedureTest.java +++ b/src/test/java/org/homer/versioner/core/procedure/RelationshipProcedureTest.java @@ -9,6 +9,8 @@ import org.neo4j.harness.junit.Neo4jRule; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -97,6 +99,52 @@ public void shouldNotCreateTheRelationshipIfDestinationIsNotAnEntity() throws Th } } + @Test + public void shouldCreateTheRelationshipsAssociatedToANewStateEachHavingOwnLabelAndProps() { + + try (Driver driver = GraphDatabase + .driver(neo4j.boltURI(), Config.build().withEncryption().toConfig()); Session session = driver.session()) { + + // Given + final Node entityA = initEntity(session); + final Node entityB = initEntity(session); + final Node entityC = initEntity(session); + final Node entityD = initEntity(session); + final Node entityE = initEntity(session); + + final String testType = "testType"; + final Long date = 593920000000L; + final String dateString = convertEpochToLocalDateTime(date).toString(); + + // When + final String query = "WITH [%d, %d, %d, %d] as list MATCH (a:Entity), (b:Entity) WHERE id(a) = %d AND any(item in list where id(b) = item) WITH a, collect(b) as bs CALL graph.versioner.relationships.create(a, bs, [{versionerLabel:\"b\",name:\"b\"},{versionerLabel:\"c\",name:\"c\"},{versionerLabel:\"d\",name:\"d\"},{versionerLabel:\"e\",name:\"e\"}]) YIELD relationship RETURN relationship"; + session.run(String.format(query,entityB.id(), entityC.id(), entityD.id(), entityE.id(), entityA.id(), dateString)); + + // Then + final String querySourceCurrent = "MATCH (e:Entity)-[r:CURRENT]->(:State)-[l:%s]->(:R) WHERE id(e) = %d RETURN l"; + final Relationship bRelationship = session.run(String.format(querySourceCurrent, "b", entityA.id())).single().get("l").asRelationship(); + + assertThat(bRelationship) + .matches(rel -> rel.containsKey("name") && rel.get("name").asString().equals("b")); + + final Relationship cRelationship = session.run(String.format(querySourceCurrent, "c", entityA.id())).single().get("l").asRelationship(); + + assertThat(cRelationship) + .matches(rel -> rel.containsKey("name") && rel.get("name").asString().equals("c")); + + + final Relationship dRelationship = session.run(String.format(querySourceCurrent, "d", entityA.id())).single().get("l").asRelationship(); + + assertThat(dRelationship) + .matches(rel -> rel.containsKey("name") && rel.get("name").asString().equals("d")); + + final Relationship eRelationship = session.run(String.format(querySourceCurrent, "e", entityA.id())).single().get("l").asRelationship(); + + assertThat(eRelationship) + .matches(rel -> rel.containsKey("name") && rel.get("name").asString().equals("e")); + } + } + @Test public void shouldCreateTheRelationshipAssociatedToANewStateHavingRequestedDate() { @@ -104,18 +152,19 @@ public void shouldCreateTheRelationshipAssociatedToANewStateHavingRequestedDate( .driver(neo4j.boltURI(), Config.build().withEncryption().toConfig()); Session session = driver.session()) { // Given - Node entityA = initEntity(session); - Node entityB = initEntity(session); - String testType = "testType"; - Long date = 593920000000L; + final Node entityA = initEntity(session); + final Node entityB = initEntity(session); + final String testType = "testType"; + final Long date = 593920000000L; + final String dateString = convertEpochToLocalDateTime(date).toString(); // When - String query = "MATCH (a:Entity), (b:Entity) WHERE id(a) = %d AND id(b) = %d WITH a, b CALL graph.versioner.relationship.create(a, b, '%s', {}, localdatetime('1988-10-27T02:46:40')) YIELD relationship RETURN relationship"; - session.run(String.format(query, entityA.id(), entityB.id(), testType)); + final String query = "MATCH (a:Entity), (b:Entity) WHERE id(a) = %d AND id(b) = %d WITH a, b CALL graph.versioner.relationship.create(a, b, '%s', {}, localdatetime('%s')) YIELD relationship RETURN relationship"; + session.run(String.format(query, entityA.id(), entityB.id(), testType, dateString)); // Then - String querySourceCurrent = "MATCH (e:Entity)-[r:CURRENT]->(:State)-[:%s]->(:R) WHERE id(e) = %d RETURN r"; - Relationship currentRelationship = session.run(String.format(querySourceCurrent, testType, entityA.id())).single().get("r").asRelationship(); + final String querySourceCurrent = "MATCH (e:Entity)-[r:CURRENT]->(:State)-[:%s]->(:R) WHERE id(e) = %d RETURN r"; + final Relationship currentRelationship = session.run(String.format(querySourceCurrent, testType, entityA.id())).single().get("r").asRelationship(); assertThat(currentRelationship) .matches(rel -> rel.containsKey("date") && rel.get("date").asLocalDateTime().equals(convertEpochToLocalDateTime(date)));