diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.nodesByLabel.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.nodesByLabel.adoc index 9e197aa554..9c007aa04d 100644 --- a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.nodesByLabel.adoc +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.nodesByLabel.adoc @@ -1,5 +1,5 @@ ¦xref::overview/apoc.trigger/apoc.trigger.nodesByLabel.adoc[apoc.trigger.nodesByLabel icon:book[]] + -`` +`apoc.trigger.nodesByLabel(labelEntries, label)` - function to filter labelEntries by label, to be used within a trigger kernelTransaction with `$assignedLabels`, `$removedLabels`, `$assigned/removedNodeProperties`. ¦label:function[] ¦label:apoc-extended[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.propertiesByKey.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.propertiesByKey.adoc index 7b818b9a6e..0648b2a8a8 100644 --- a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.propertiesByKey.adoc +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.propertiesByKey.adoc @@ -1,5 +1,5 @@ ¦xref::overview/apoc.trigger/apoc.trigger.propertiesByKey.adoc[apoc.trigger.propertiesByKey icon:book[]] + -`` +`apoc.trigger.propertiesByKey(propertyEntries, key)` - function to filter propertyEntries by property-key, to be used within a trigger kernelTransaction with `$assignedNode/RelationshipProperties` and `$removedNode/RelationshipProperties`. Returns [`old`,`new`,`key`,`node`,`relationship`]. ¦label:function[] ¦label:apoc-extended[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode-lite.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode-lite.csv new file mode 100644 index 0000000000..f9dce372d0 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode-lite.csv @@ -0,0 +1,2 @@ +¦signature +¦apoc.trigger.nodesByLabel(labelEntries :: ANY?, label :: STRING?) :: (LIST? OF ANY?) diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode.adoc new file mode 100644 index 0000000000..92f6e01739 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode.adoc @@ -0,0 +1,5 @@ +¦xref::overview/apoc.trigger/apoc.trigger.toNode.adoc[apoc.trigger.toNode icon:book[]] + + +`apoc.trigger.toNode(node, removedLabels, removedNodeProperties)` - function to rebuild a node as a virtual one, to be used in triggers with a not 'afterAsync' phase. +¦label:function[] +¦label:apoc-extended[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode.csv new file mode 100644 index 0000000000..7db479ced3 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toNode.csv @@ -0,0 +1,2 @@ +¦type¦qualified name¦signature¦description +¦function¦apoc.trigger.nodesByLabel¦apoc.trigger.nodesByLabel(labelEntries :: ANY?, label :: STRING?) :: (LIST? OF ANY?)¦ diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship-lite.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship-lite.csv new file mode 100644 index 0000000000..f9dce372d0 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship-lite.csv @@ -0,0 +1,2 @@ +¦signature +¦apoc.trigger.nodesByLabel(labelEntries :: ANY?, label :: STRING?) :: (LIST? OF ANY?) diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship.adoc new file mode 100644 index 0000000000..2756730040 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship.adoc @@ -0,0 +1,5 @@ +¦xref::overview/apoc.trigger/apoc.trigger.toRelationship.adoc[apoc.trigger.toRelationship icon:book[]] + + +`apoc.trigger.toRelationship(rel, removedRelationshipProperties)` - function to rebuild a relationship as a virtual one, to be used in triggers with a not 'afterAsync' phase. +¦label:function[] +¦label:apoc-extended[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship.csv new file mode 100644 index 0000000000..7db479ced3 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.trigger.toRelationship.csv @@ -0,0 +1,2 @@ +¦type¦qualified name¦signature¦description +¦function¦apoc.trigger.nodesByLabel¦apoc.trigger.nodesByLabel(labelEntries :: ANY?, label :: STRING?) :: (LIST? OF ANY?)¦ diff --git a/docs/asciidoc/modules/ROOT/nav.adoc b/docs/asciidoc/modules/ROOT/nav.adoc index 8ccd02b742..3ba70952fc 100644 --- a/docs/asciidoc/modules/ROOT/nav.adoc +++ b/docs/asciidoc/modules/ROOT/nav.adoc @@ -79,6 +79,7 @@ include::partial$generated-documentation/nav.adoc[] * xref:background-operations/index.adoc[] ** xref::background-operations/apoc-load-directory-async.adoc[] + ** xref:background-operations/triggers.adoc[] * xref:database-introspection/index.adoc[] ** xref::database-introspection/config.adoc[] diff --git a/docs/asciidoc/modules/ROOT/pages/background-operations/index.adoc b/docs/asciidoc/modules/ROOT/pages/background-operations/index.adoc index ef2cc77c65..f7fa1bef90 100644 --- a/docs/asciidoc/modules/ROOT/pages/background-operations/index.adoc +++ b/docs/asciidoc/modules/ROOT/pages/background-operations/index.adoc @@ -5,4 +5,5 @@ It's possible to define listeners on one or more folders which trigger the executing of custom cypher queries if changes are observed: -* xref::background-operations/apoc-load-directory-async.adoc[] \ No newline at end of file +* xref::background-operations/apoc-load-directory-async.adoc[] +* xref:background-operations/triggers.adoc[] \ No newline at end of file diff --git a/docs/asciidoc/modules/ROOT/pages/background-operations/triggers.adoc b/docs/asciidoc/modules/ROOT/pages/background-operations/triggers.adoc new file mode 100644 index 0000000000..631ddb16b9 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/background-operations/triggers.adoc @@ -0,0 +1,16 @@ +APOC Core provides a set of procedures for running Cypher queries that are called when data in Neo4j is changed (created, updated, deleted). + +https://neo4j.com/docs/apoc/current/background-operations/triggers/[See here for more info]. + +In addition, APOC Extended provides some helper procedures to more easily manipulate Cypher queries and solve some transaction use cases that cannot be solved otherwise. + +== Helper Functions + +[separator=¦,opts=header,cols="5,1m,1m"] +|=== +¦Qualified Name¦Type¦Release +include::example$generated-documentation/apoc.trigger.nodesByLabel.adoc[] +include::example$generated-documentation/apoc.trigger.propertiesByKey.adoc[] +include::example$generated-documentation/apoc.trigger.toNode.adoc[] +include::example$generated-documentation/apoc.trigger.toRelationship.adoc[] +|=== \ No newline at end of file diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.trigger/apoc.trigger.toNode.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.trigger/apoc.trigger.toNode.adoc new file mode 100644 index 0000000000..50c9ab2e36 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.trigger/apoc.trigger.toNode.adoc @@ -0,0 +1,28 @@ + += apoc.trigger.toNode +:description: This section contains reference documentation for the apoc.trigger.toNode function. + +label:function[] label:apoc-extended[] + +== Signature + +[source] +---- +apoc.trigger.toNode(node :: NODE, removedLabels :: MAP, removedNodeProperties :: MAP) :: RELATIONSHIPH +---- + +== Input parameters +[.procedures, opts=header] +|=== +| Name | Type | Default +|node|NODE|null +|removedLabels|MAP|null +|removedNodeProperties|MAP|null +|=== + +[[usage-apoc.trigger.nodesByLabel]] +== Usage Examples +include::partial$usage/apoc.trigger.toNode.adoc[] + +xref::background-operations/triggers.adoc[More documentation of apoc.trigger.toNode,role=more information] + diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.trigger/apoc.trigger.toRelationship.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.trigger/apoc.trigger.toRelationship.adoc new file mode 100644 index 0000000000..d1be3b96ea --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.trigger/apoc.trigger.toRelationship.adoc @@ -0,0 +1,27 @@ + += apoc.trigger.toRelationship +:description: This section contains reference documentation for the apoc.trigger.toRelationship function. + +label:function[] label:apoc-extended[] + +== Signature + +[source] +---- +apoc.trigger.toRelationship(rel :: RELATIONSHIP, removedRelationshipProperties :: MAP) :: RELATIONSHIP +---- + +== Input parameters +[.procedures, opts=header] +|=== +| Name | Type | Default +|rel|RELATIONSHIP|null +|removedRelationshipProperties|MAP|null +|=== + +[[usage-apoc.trigger.nodesByLabel]] +== Usage Examples +include::partial$usage/apoc.trigger.toRelationship.adoc[] + +xref::background-operations/triggers.adoc[More documentation of apoc.trigger.toNode,role=more information] + diff --git a/docs/asciidoc/modules/ROOT/partials/triggers.adoc b/docs/asciidoc/modules/ROOT/partials/triggers.adoc deleted file mode 100644 index b28a361ac5..0000000000 --- a/docs/asciidoc/modules/ROOT/partials/triggers.adoc +++ /dev/null @@ -1,17 +0,0 @@ -By default triggers are disabled. -We can enable them by setting the following property in `apoc.conf`: - -.apoc.conf -[source,properties] ----- -apoc.trigger.enabled=true -apoc.trigger.refresh=60000 ----- - -.Description -[opts=header,cols="3"] -|=== -| Option Key | Value | Description -| apoc.trigger.enabled | true/false, default false | Enable/Disable the feature -| apoc.trigger.refresh | number, default 60000 | Interval in ms after which a replication check is triggered across all cluster nodes -|=== \ No newline at end of file diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.nodesByLabel.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.nodesByLabel.adoc index 48d8b5f4a8..4ae7c688b4 100644 --- a/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.nodesByLabel.adoc +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.nodesByLabel.adoc @@ -6,14 +6,15 @@ create constraint for (p:Person) require p.id is unique; ---- -This function is used inside an apoc.trigger.add Cypher statement. +This function is intended to be used inside an apoc.trigger.install Cypher statement. We can use it to conditionally run Cypher statements when labels are added or removed or when properties are added or removed. For example, we add an `id` property to all `Person` nodes that is the lower case value of the `name` property of that node, by defining the following trigger: [source,cypher] ---- -CALL apoc.trigger.add( +CALL apoc.trigger.install( + 'neo4j', 'lowercase', 'UNWIND apoc.trigger.nodesByLabel($assignedLabels,"Person") AS n SET n.id = toLower(n.name)', diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.propertiesByKey.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.propertiesByKey.adoc index 1d087ba6cd..a51cf5a362 100644 --- a/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.propertiesByKey.adoc +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.propertiesByKey.adoc @@ -1,11 +1,12 @@ -This function is used inside an apoc.trigger.add.adoc Cypher statement. +This function is intended to be used inside an apoc.trigger.install Cypher statement. We can use it to conditionally run Cypher statements when properties are added or removed. For example, we can connect nodes with a `genre` property to a `Genre` node, with the following trigger: [source,cypher] ---- -CALL apoc.trigger.add( +CALL apoc.trigger.install( + 'neo4j', 'triggerTest', 'UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties, "genre") as prop WITH prop.node as n diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.toNode.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.toNode.adoc new file mode 100644 index 0000000000..e3680d873a --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.toNode.adoc @@ -0,0 +1,46 @@ +This function is intended to be used within an `apoc.trigger.install` Cypher statement. + +If we want to create a 'before' or 'after' trigger query using `$deletedNodes`, and retrieve entity information such as labels and/or properties, we cannot use the classic Cypher functions labels() and properties(). +Instead, we have to leverage virtual nodes through the function `apoc.trigger.toNode(node, $removedLabels, $removedNodeProperties)`. + +For example, to create a new `Report` node with a list of deleted node IDs and all the labels retrieved for each deleted node, we can execute: +[source,cypher] +---- +CALL apoc.trigger.install( + 'neo4j', 'myTrigger', + "UNWIND $deletedNodes as deletedNode + WITH apoc.trigger.toNode(deletedNode, $removedLabels, $removedNodeProperties) AS deletedNode + CALL apoc.merge.node( + ['Report'], + {labels: apoc.node.labels(deletedNode)}, + {created: datetime()}, + {updated: datetime()} + ) YIELD node AS report + WITH report, deletedNode + SET report.deletedIds = coalesce(report.deletedIds, [])+[id(deletedNode)]" , + {phase:'before'} +); +---- + +Now, let's create and delete a `Movie` node: + +[source,cypher] +---- +CREATE (:Movie {title: "The White Tiger"}); +MATCH (movie:Movie {title: "The White Tiger"}) DELETE movie; +---- + +Finally, let's check the `Report` node: + +[source,cypher] +---- +MATCH (report:Report {labels: ['Movie']}) +RETURN report; +---- + +.Results +[opts="header"] +|=== +| report +| (:Report {"created": "2024-12-12T08:33:27.188000000Z", "deletedIds": [12], "labels": ["Movie"]}) +|=== diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.toRelationship.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.toRelationship.adoc new file mode 100644 index 0000000000..5dcab9338b --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.trigger.toRelationship.adoc @@ -0,0 +1,48 @@ +This function is intended to be used within an `apoc.trigger.install` Cypher statement. + +If we want to create a 'before' or 'after' trigger query using `$deletedRelationships`, and retrieve entity information such as the type and/or properties, we cannot use the classic Cypher functions type() and properties(). +Instead, we have to leverage virtual relationships through the function `apoc.trigger.toRelationship(rel, $removedRelationshipProperties)`. + +For example, to create a new `Report` node with a list of deleted relationship IDs and the type retrieved for each deleted relationship, we can execute: +[source,cypher] +---- +CALL apoc.trigger.install( + 'neo4j', 'myTrigger', + "UNWIND $deletedRelationships as deletedRel + WITH apoc.trigger.toRelationship(deletedRel, $removedRelationshipProperties) AS deletedRel + CALL apoc.merge.node( + ['Report'], + {type: apoc.rel.type(deletedRel)}, + {created: datetime()}, + {updated: datetime()} + ) YIELD node AS report + WITH report, deletedRel + SET report.deletedIds = coalesce(report.deletedIds, [])+[id(deletedRel)]" , + {phase:'before'} +); +---- + +Now, let's create and delete a `IN_GENRE` relationship between a `Movie` node and a `Genre` node: + +[source,cypher] +---- +MERGE (movie:Movie {title: "The White Tiger"}) +MERGE (genre:Genre {name: "Triller"}) +MERGE (movie)-[IN_GENRE]->(genre); +MATCH (movie:Movie {title: "The White Tiger"})-[r:IN_GENRE]->(genre:Genre {name: "Triller"}) DELETE r; +---- + +Finally, let's check the `Report` node: + +[source,cypher] +---- +MATCH (report:Report {labels: ['IN_GENRE']}) +RETURN report; +---- + +.Results +[opts="header"] +|=== +| report +| (:Report {"created": "2024-12-12T08:33:27.188000000Z", "deletedIds": [12], "type": "IN_GENRE"}) +|=== diff --git a/extended/src/main/java/apoc/ml/OpenAI.java b/extended/src/main/java/apoc/ml/OpenAI.java index 1a659f21ef..4bc4814b33 100644 --- a/extended/src/main/java/apoc/ml/OpenAI.java +++ b/extended/src/main/java/apoc/ml/OpenAI.java @@ -3,11 +3,11 @@ import apoc.ApocConfig; import apoc.Extended; import apoc.result.MapResult; -import apoc.util.ExtendedMapUtils; import apoc.util.ExtendedUtil; import apoc.util.JsonUtil; import apoc.util.Util; import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.neo4j.graphdb.security.URLAccessChecker; import org.neo4j.procedure.Context; @@ -225,7 +225,7 @@ public Stream completion(@Name("prompt") String prompt, @Name("api_ke public Stream chatCompletion(@Name("messages") List> messages, @Name("api_key") String apiKey, @Name(value = "configuration", defaultValue = "{}") Map configuration) throws Exception { boolean failOnError = isFailOnError(configuration); if (checkNullInput(messages, failOnError)) return Stream.empty(); - messages = messages.stream().filter(ExtendedMapUtils::isNotEmpty).toList(); + messages = messages.stream().filter(MapUtils::isNotEmpty).toList(); if (checkEmptyInput(messages, failOnError)) return Stream.empty(); configuration.putIfAbsent("model", GPT_4O_MODEL); return executeRequest(apiKey, configuration, "chat/completions", (String) configuration.get("model"), "messages", messages, "$", apocConfig, urlAccessChecker) diff --git a/extended/src/main/java/apoc/util/ExtendedMapUtils.java b/extended/src/main/java/apoc/util/ExtendedMapUtils.java index 06b8ce9c6e..c6b388b07d 100644 --- a/extended/src/main/java/apoc/util/ExtendedMapUtils.java +++ b/extended/src/main/java/apoc/util/ExtendedMapUtils.java @@ -11,8 +11,4 @@ public static int size(final Map map) { public static boolean isEmpty(final Map map) { return map == null || map.isEmpty(); } - - public static boolean isNotEmpty(final Map map) { - return !isEmpty(map); - } }