Skip to content

Commit 0c26ea9

Browse files
authored
Fixes #2181: extend apoc.create.virtual.fromNode(node,[propertyNames]) to add new virtual properties (#4376) (#4389)
* Fixes #2181: extend apoc.create.virtual.fromNode(node,[propertyNames]) to add new virtual properties * added extendedCypher5.txt proc * added procedure in extendedCypher25.txt
1 parent 17b7a61 commit 0c26ea9

File tree

6 files changed

+285
-2
lines changed

6 files changed

+285
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
= apoc.create.virtual.fromNodeExtended
2+
:description: This section contains reference documentation for the apoc.create.virtual.fromNodeExtended function.
3+
4+
label:function[] label:apoc-extended[]
5+
6+
[.emphasis]
7+
apoc.create.virtual.fromNodeExtended(node, [propertyNames]) returns a virtual node built from an existing node with only the requested properties
8+
9+
== Signature
10+
11+
[source]
12+
----
13+
apoc.create.virtual.fromNodeExtended(node :: NODE?, propertyNames :: LIST? OF STRING?, additionalProperties :: MAP, ?config :: MAP?) :: (NODE?)
14+
----
15+
16+
== Input parameters
17+
[.procedures, opts=header]
18+
|===
19+
| Name | Type | Default
20+
|node|NODE?|null
21+
|propertyNames|LIST? OF STRING?|null
22+
|additionalProperties|MAP?|{}
23+
|config|MAP?|{}
24+
|===
25+
26+
[[config-apoc.create.virtual.fromNodeExtended]]
27+
== Configuration parameters
28+
29+
The procedures support the following config parameters:
30+
31+
.Config parameters
32+
[opts=header]
33+
|===
34+
| name | type | default | description
35+
| wrapNodeIds | boolean | false | By default, this function will change the id of the node to a negative number which is representative of Virtual Nodes. We can set it to true in order to retain the original id.
36+
|===
37+
38+
[[usage-apoc.create.virtual.fromNodeExtended]]
39+
== Usage Examples
40+
The examples in this section are based on the following graph:
41+
42+
[source,cypher]
43+
----
44+
CREATE (a:Account {type: 'checking', ownerName: 'Maria Perez', ownerId: '123456789', accountNumber: 101010101, routingNumber: 10101010, amount: 1000.00, bank: 'Best Bank'});
45+
CREATE (p:Person {name: 'Jane Doe', birthdate: date('1990-01-13'), favoriteColor: 'green', favoriteDessert: 'ice cream', favoriteMusic: 'classical', favoriteBand: 'The Beatles', favoriteVacation: 'beach', favoriteAnimal: 'horse', favoriteBeverage: 'coffee', favoriteFlower: 'lily'});
46+
----
47+
48+
The apoc.create.virtual.fromNodeExtended procedure provides a way to only visualize or return data that is needed, hiding any unnecessary or sensitive pieces.
49+
50+
The example below shows how we can use the procedure to return only the non-sensitive properties from the node above:
51+
52+
// tag::tabs[]
53+
[.tabs]
54+
.apoc.create.virtual.fromNodeExtended
55+
[source,cypher]
56+
----
57+
MATCH (a:Account {accountNumber: 101010101})
58+
RETURN apoc.create.virtual.fromNodeExtended(a, ['type','bank']);
59+
----
60+
// end::tabs[]
61+
62+
.Results
63+
[opts="header"]
64+
|===
65+
| account
66+
| {"type":"checking","bank":"Best Bank"}
67+
|===
68+
69+
The apoc.create.virtual.fromNodeExtended procedure can also be used to simplify nodes with many properties by only displaying ones that are important to the query.
70+
71+
The example below shows an example of this use:
72+
73+
.apoc.create.virtual.fromNodeExtended
74+
[source,cypher]
75+
----
76+
MATCH (p:Person {name: 'Jane Doe'})
77+
RETURN apoc.create.virtual.fromNodeExtended(p, ['favoriteColor','favoriteAnimal','favoriteMusic']);
78+
----
79+
80+
.Results
81+
[opts="header"]
82+
|===
83+
|favorites
84+
|{"favoriteAnimal":"horse", "favoriteMusic":"classical", "favoriteColor":"green"}
85+
|===
86+
87+
We can also set additional properties via the 3rd parameter.
88+
The example below shows an example of this use:
89+
90+
.apoc.create.virtual.fromNodeExtended
91+
[source,cypher]
92+
----
93+
MATCH (p:Person {name: 'Jane Doe'})
94+
RETURN apoc.create.virtual.fromNodeExtended(p, ['favoriteColor','favoriteAnimal','favoriteMusic'], {foo: 'bar', alpha: 1});
95+
----
96+
97+
.Results
98+
[opts="header"]
99+
|===
100+
|favorites
101+
|{"favoriteAnimal":"horse", "favoriteMusic":"classical", "favoriteColor":"green", "foo":"bar", "alpha":1}
102+
|===
103+
104+
[[wrapping-nodes]]
105+
=== Wrapping nodes
106+
By default, this function will change the id of the node to a negative number which is representative of Virtual Nodes.
107+
In order to retain the original id, use the config item `{ wrapNodeIds: true }`.
108+
109+
.apoc.create.virtual.fromNodeExtended
110+
[source,cypher]
111+
----
112+
CREATE (p:Person {name: 'Jane Doe'})
113+
WITH apoc.create.virtual.fromNodeExtended(p, ['name'], {}, { wrapNodeIds: true }) AS node
114+
RETURN id(node) AS id;
115+
----
116+
117+
.Results
118+
[opts="header"]
119+
|===
120+
| id
121+
| 1
122+
|===
123+
124+
125+
xref::virtual-resource/index.adoc[More documentation of apoc.create.virtual.fromNodeExtended,role=more information]

docs/asciidoc/modules/ROOT/pages/virtual-nodes-and-relationships/index.adoc

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
[[virtual-nodes-and-relationships]]
22
= Virtual Nodes and Relationships
33

4+
Virtual Nodes and Relationships do not exist in the graph, they are only returned by a query, and can be used to represent a graph projection.
45

6+
They can be used to **visually project data**, for example aggregating relationships into one, or collapsing intermediate nodes into virtual relationships.
7+
We could project a citation graph into a virtual author-author or paper-paper graph with aggregated relationships between them, or even turn Twitter data into a user-user mention graph.
58

9+
We can **combine** real and virtual entities, for example by creating a virtual relationship between two real nodes or creating a virtual relationship from a virtual node to a real node.
10+
11+
12+
== Create virtual entities
13+
14+
This section includes:
15+
16+
* xref::overview/apoc.create/apoc.create.virtual.fromNodeExtended.adoc[apoc.create.virtual.fromNodeExtended]
17+
18+
19+
== Returns virtual entities without some defined properties
620

721
This section includes:
822

923
* xref::overview/apoc.graph/apoc.graph.filterPropertiesProcedure.adoc[apoc.graph.filterProperties (procedure)]
1024
* xref::overview/apoc.graph/apoc.graph.filterProperties.adoc[apoc.graph.filterProperties (aggregation function)]
1125

12-
1326
We can filter some properties of nodes and relationships present in a subgraph using the `apoc.graph.filterProperties` procedure,
1427
or the analogous aggregation function.
1528

extended/import/keystore-name.pkcs12

481 Bytes
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package apoc.create;
2+
3+
import apoc.Extended;
4+
import apoc.result.VirtualNode;
5+
import org.neo4j.graphdb.Node;
6+
import org.neo4j.procedure.Description;
7+
import org.neo4j.procedure.Name;
8+
import org.neo4j.procedure.UserFunction;
9+
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
@Extended
14+
public class CreateExtended {
15+
16+
@UserFunction("apoc.create.virtual.fromNodeExtended")
17+
@Description(
18+
"Returns a virtual `NODE` from the given existing `NODE`. The virtual `NODE` only contains the requested properties.")
19+
public Node virtualFromNodeFunction(
20+
@Name(value = "node", description = "The node to generate a virtual node from.") Node node,
21+
@Name(value = "propertyNames", description = "The properties to copy to the virtual node.") List<String> propertyNames,
22+
@Name(value = "additionalProperties", defaultValue = "{}", description = "Additional properties to add to the virtual node") Map<String, Object> additionalProperties,
23+
@Name(value = "config", defaultValue = "{}", description = "{ wrapNodeIds = false :: BOOLEAN }") Map<String, Object> config) {
24+
VirtualNode virtualNode = new VirtualNode(node, propertyNames);
25+
additionalProperties.forEach(virtualNode::setProperty);
26+
return virtualNode;
27+
}
28+
}

extended/src/main/resources/extended.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,5 @@ apoc.vectordb.custom
279279
apoc.vectordb.configure
280280
apoc.kafka.consume
281281
apoc.kafka.publish
282-
apoc.kafka.publish.sync
282+
apoc.kafka.publish.sync
283+
apoc.create.virtual.fromNodeExtended
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package apoc.create;
2+
3+
import apoc.util.ExtendedTestUtil;
4+
import apoc.util.TestUtil;
5+
import org.junit.After;
6+
import org.junit.Before;
7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.neo4j.graphdb.Node;
10+
import org.neo4j.test.rule.DbmsRule;
11+
import org.neo4j.test.rule.ImpermanentDbmsRule;
12+
13+
import java.util.Map;
14+
15+
import static apoc.result.VirtualNode.ERROR_NODE_NULL;
16+
import static apoc.util.TestUtil.testCall;
17+
import static org.junit.Assert.*;
18+
import static org.neo4j.graphdb.Label.label;
19+
20+
public class CreateExtendedTest {
21+
22+
@Rule
23+
public DbmsRule db = new ImpermanentDbmsRule();
24+
25+
@Before
26+
public void setUp() {
27+
TestUtil.registerProcedure(db, Create.class, CreateExtended.class);
28+
}
29+
30+
@After
31+
public void teardown() {
32+
db.shutdown();
33+
}
34+
35+
@Test
36+
public void testVirtualFromNodeFunction() {
37+
testCall(
38+
db,
39+
"""
40+
CREATE (n:Person{name:'Vincent', born: 1974} )
41+
RETURN apoc.create.virtual.fromNodeExtended(n, ['name']) AS node
42+
""",
43+
(row) -> {
44+
Node node = (Node) row.get("node");
45+
assertTrue(node.getId() < 0);
46+
commonCreateNodeAssertions(node);
47+
});
48+
}
49+
50+
@Test
51+
public void testVirtualFromNodeWithAdditionalPropertiesFunction() {
52+
testCall(
53+
db,
54+
"""
55+
CREATE (n:Person {name:'Vincent', born: 1974} )
56+
RETURN apoc.create.virtual.fromNodeExtended(n, ['name'], {alpha: 1, foo: 'bar'}) AS node
57+
""",
58+
(row) -> {
59+
Node node = (Node) row.get("node");
60+
assertEquals(1L, node.getProperty("alpha"));
61+
assertEquals("bar", node.getProperty("foo"));
62+
assertTrue(node.getId() < 0);
63+
commonCreateNodeAssertions(node);
64+
});
65+
}
66+
67+
@Test
68+
public void testVirtualFromNodeShouldNotEditOriginalOne() {
69+
db.executeTransactionally("CREATE (n:Person {name:'toUpdate'})");
70+
71+
testCall(
72+
db,
73+
"""
74+
MATCH (n:Person {name:'toUpdate'})
75+
WITH apoc.create.virtual.fromNodeExtended(n, ['name']) as nVirtual
76+
CALL apoc.create.setProperty(nVirtual, 'ajeje', 0) YIELD node RETURN node
77+
""",
78+
(row) -> {
79+
Node node = (Node) row.get("node");
80+
assertEquals("toUpdate", node.getProperty("name"));
81+
assertEquals(0L, node.getProperty("ajeje"));
82+
});
83+
84+
testCall(
85+
db,
86+
"""
87+
MATCH (n:Person {name:'toUpdate'})
88+
WITH apoc.create.virtual.fromNodeExtended(n, ['name']) as node
89+
SET node.ajeje = 0 RETURN node""",
90+
(row) -> {
91+
Node node = (Node) row.get("node");
92+
assertEquals("toUpdate", node.getProperty("name"));
93+
assertFalse(node.hasProperty("ajeje"));
94+
});
95+
96+
testCall(db, "MATCH (node:Person {name:'toUpdate'}) RETURN node", (row) -> {
97+
Node node = (Node) row.get("node");
98+
assertEquals("toUpdate", node.getProperty("name"));
99+
assertFalse(node.hasProperty("ajeje"));
100+
});
101+
}
102+
103+
@Test
104+
public void testValidationNodes() {
105+
ExtendedTestUtil.assertFails(db,
106+
"RETURN apoc.create.virtual.fromNodeExtended(null, ['name']) as node",
107+
Map.of(),
108+
ERROR_NODE_NULL);
109+
}
110+
111+
private static void commonCreateNodeAssertions(Node node) {
112+
assertTrue(node.hasLabel(label("Person")));
113+
assertEquals("Vincent", node.getProperty("name"));
114+
assertNull(node.getProperty("born"));
115+
}
116+
}

0 commit comments

Comments
 (0)