Skip to content

Commit 9135eeb

Browse files
committed
SURF-766 Add additionalLabels and additionalProperties config to apoc.create.virtual.fromNode
Extend the config map of apoc.create.virtual.fromNode to support additionalLabels (LIST<STRING>) and additionalProperties (MAP), allowing users to annotate virtual nodes with extra labels and properties at creation time without requiring write mode. https://linear.app/neo4j/issue/SURF-766/apoc-add-a-virtual-label-to-a-result-node-to-highlight-eg-potential
1 parent 1a9c969 commit 9135eeb

3 files changed

Lines changed: 122 additions & 3 deletions

File tree

core/src/main/java/apoc/create/Create.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,33 @@ public Node virtualFromNodeFunction(
417417
@Name(value = "node", description = "The node to generate a virtual node from.") Node node,
418418
@Name(value = "propertyNames", description = "The properties to copy to the virtual node.")
419419
List<String> propertyNames,
420-
@Name(value = "config", defaultValue = "{}", description = "{ wrapNodeIds = false :: BOOLEAN }")
420+
@Name(
421+
value = "config",
422+
defaultValue = "{}",
423+
description =
424+
"{ wrapNodeIds = false :: BOOLEAN, additionalLabels = [] :: LIST<STRING>, additionalProperties = {} :: MAP }")
421425
Map<String, Object> config) {
422-
return new VirtualNode(node, propertyNames, Util.toBoolean(config.get("wrapNodeIds")));
426+
VirtualNode virtualNode = new VirtualNode(node, propertyNames, Util.toBoolean(config.get("wrapNodeIds")));
427+
428+
Object labelsValue = config.get("additionalLabels");
429+
if (labelsValue instanceof List<?> labels) {
430+
for (Object l : labels) {
431+
if (l instanceof String labelName) {
432+
virtualNode.addLabel(Label.label(labelName));
433+
}
434+
}
435+
}
436+
437+
Object propsValue = config.get("additionalProperties");
438+
if (propsValue instanceof Map<?, ?> props) {
439+
props.forEach((key, value) -> {
440+
if (key instanceof String propertyName) {
441+
virtualNode.setProperty(propertyName, value);
442+
}
443+
});
444+
}
445+
446+
return virtualNode;
423447
}
424448

425449
@Procedure("apoc.create.vNodes")

core/src/test/java/apoc/create/CreateTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,101 @@ void testVirtualFromNodeFunctionWithWrapping() {
484484
});
485485
}
486486

487+
@Test
488+
void testVirtualFromNodeWithAdditionalLabels() {
489+
testCall(
490+
db,
491+
"""
492+
CREATE (n:Person{name:'Vincent', born: 1974} )
493+
RETURN apoc.create.virtual.fromNode(n, ['name'], { additionalLabels: ['Suspect', 'Highlighted'] }) AS node
494+
""",
495+
(row) -> {
496+
Node node = (Node) row.get("node");
497+
498+
assertTrue(node.hasLabel(label("Person")));
499+
assertTrue(node.hasLabel(label("Suspect")));
500+
assertTrue(node.hasLabel(label("Highlighted")));
501+
assertEquals("Vincent", node.getProperty("name"));
502+
assertNull(node.getProperty("born"));
503+
});
504+
}
505+
506+
@Test
507+
void testVirtualFromNodeWithAdditionalProperties() {
508+
testCall(
509+
db,
510+
"""
511+
CREATE (n:Person{name:'Vincent', born: 1974} )
512+
RETURN apoc.create.virtual.fromNode(n, ['name'], { additionalProperties: { flagged: true, score: 42 } }) AS node
513+
""",
514+
(row) -> {
515+
Node node = (Node) row.get("node");
516+
517+
assertTrue(node.hasLabel(label("Person")));
518+
assertEquals("Vincent", node.getProperty("name"));
519+
assertEquals(true, node.getProperty("flagged"));
520+
assertEquals(42L, node.getProperty("score"));
521+
assertNull(node.getProperty("born"));
522+
});
523+
}
524+
525+
@Test
526+
void testVirtualFromNodeWithAdditionalLabelsAndProperties() {
527+
testCall(
528+
db,
529+
"""
530+
CREATE (n:Person{name:'Vincent', born: 1974} )
531+
RETURN apoc.create.virtual.fromNode(n, ['name'], {
532+
wrapNodeIds: true,
533+
additionalLabels: ['Fraud'],
534+
additionalProperties: { risk: 'high' }
535+
}) AS node
536+
""",
537+
(row) -> {
538+
Node node = (Node) row.get("node");
539+
540+
assertTrue(node.hasLabel(label("Person")));
541+
assertTrue(node.hasLabel(label("Fraud")));
542+
assertTrue(node.getId() >= 0);
543+
assertEquals("Vincent", node.getProperty("name"));
544+
assertEquals("high", node.getProperty("risk"));
545+
assertNull(node.getProperty("born"));
546+
});
547+
}
548+
549+
@Test
550+
void testVirtualFromNodeWithEmptyAdditionalLabelsAndProperties() {
551+
testCall(
552+
db,
553+
"""
554+
CREATE (n:Person{name:'Vincent', born: 1974} )
555+
RETURN apoc.create.virtual.fromNode(n, ['name'], { additionalLabels: [], additionalProperties: {} }) AS node
556+
""",
557+
(row) -> {
558+
Node node = (Node) row.get("node");
559+
560+
assertTrue(node.hasLabel(label("Person")));
561+
assertEquals(1, Iterables.count(node.getLabels()));
562+
assertEquals("Vincent", node.getProperty("name"));
563+
assertNull(node.getProperty("born"));
564+
});
565+
}
566+
567+
@Test
568+
void testVirtualFromNodeAdditionalPropertyDoesNotOverwriteCopied() {
569+
testCall(
570+
db,
571+
"""
572+
CREATE (n:Person{name:'Vincent', born: 1974} )
573+
RETURN apoc.create.virtual.fromNode(n, ['name'], { additionalProperties: { name: 'Override' } }) AS node
574+
""",
575+
(row) -> {
576+
Node node = (Node) row.get("node");
577+
578+
assertEquals("Override", node.getProperty("name"));
579+
});
580+
}
581+
487582
@Test
488583
void testVirtualFromNodeShouldNotEditOriginalOne() {
489584
db.executeTransactionally("CREATE (n:Person {name:'toUpdate'})");

core/src/test/resources/functions/common/functions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,7 @@
13601360
},
13611361
{
13621362
"name": "config",
1363-
"description": "{ wrapNodeIds = false :: BOOLEAN }",
1363+
"description": "{ wrapNodeIds = false :: BOOLEAN, additionalLabels = [] :: LIST<STRING>, additionalProperties = {} :: MAP }",
13641364
"isDeprecated": false,
13651365
"default": "DefaultParameterValue{value={}, type=MAP}",
13661366
"type": "MAP"

0 commit comments

Comments
 (0)