diff --git a/core/src/main/java/apoc/ApocExtensionFactory.java b/core/src/main/java/apoc/ApocExtensionFactory.java index b091460baf..c4e1bb91ef 100644 --- a/core/src/main/java/apoc/ApocExtensionFactory.java +++ b/core/src/main/java/apoc/ApocExtensionFactory.java @@ -31,6 +31,7 @@ import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.kernel.lifecycle.Lifecycle; import org.neo4j.kernel.lifecycle.LifecycleAdapter; +import org.neo4j.kernel.monitoring.DatabaseEventListeners; import org.neo4j.logging.Log; import org.neo4j.logging.internal.LogService; import org.neo4j.scheduler.JobScheduler; @@ -62,6 +63,7 @@ public interface Dependencies { LogService log(); AvailabilityGuard availabilityGuard(); DatabaseManagementService databaseManagementService(); + DatabaseEventListeners databaseEventListeners(); ApocConfig apocConfig(); TTLConfig ttlConfig(); GlobalProcedures globalProceduresRegistry(); diff --git a/core/src/main/java/apoc/CoreApocGlobalComponents.java b/core/src/main/java/apoc/CoreApocGlobalComponents.java index 6c0c7a9c00..06910ad84a 100644 --- a/core/src/main/java/apoc/CoreApocGlobalComponents.java +++ b/core/src/main/java/apoc/CoreApocGlobalComponents.java @@ -50,6 +50,10 @@ public Collection getContextClasses() { @Override public Iterable getListeners(GraphDatabaseAPI db, ApocExtensionFactory.Dependencies dependencies) { - return Collections.singleton(new CypherInitializer(db, dependencies.log().getUserLog(CypherInitializer.class))); + return Collections.singleton(new CypherInitializer(db, + dependencies.log().getUserLog(CypherInitializer.class), + dependencies.databaseManagementService(), + dependencies.databaseEventListeners()) + ); } } diff --git a/core/src/main/java/apoc/cypher/CypherInitializer.java b/core/src/main/java/apoc/cypher/CypherInitializer.java index 4a4c6e409b..09063dcd27 100644 --- a/core/src/main/java/apoc/cypher/CypherInitializer.java +++ b/core/src/main/java/apoc/cypher/CypherInitializer.java @@ -19,6 +19,7 @@ package apoc.cypher; import apoc.ApocConfig; +import apoc.SystemLabels; import apoc.util.Util; import apoc.version.Version; import org.apache.commons.configuration2.Configuration; @@ -26,12 +27,19 @@ import org.neo4j.common.DependencyResolver; import org.neo4j.configuration.Config; import org.neo4j.configuration.GraphDatabaseSettings; +import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.dbms.database.SystemGraphComponent.Status; import org.neo4j.dbms.database.SystemGraphComponents; import org.neo4j.internal.helpers.collection.Iterators; import org.neo4j.kernel.api.procedure.GlobalProcedures; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.event.DatabaseEventContext; +import org.neo4j.graphdb.event.DatabaseEventListener; import org.neo4j.kernel.availability.AvailabilityListener; import org.neo4j.kernel.internal.GraphDatabaseAPI; +import org.neo4j.kernel.monitoring.DatabaseEventListeners; import org.neo4j.logging.Log; import java.util.Collection; @@ -39,6 +47,9 @@ import java.util.ConcurrentModificationException; import java.util.Map; import java.util.TreeMap; +import java.util.function.BiConsumer; + +import static apoc.SystemPropertyKeys.database; import java.util.function.BooleanSupplier; @@ -49,15 +60,22 @@ public class CypherInitializer implements AvailabilityListener { private final GlobalProcedures procs; private final DependencyResolver dependencyResolver; private final String defaultDb; + private final DatabaseManagementService databaseManagementService; + private final DatabaseEventListeners databaseEventListeners; + /** * indicates the status of the initializer, to be used for tests to ensure initializer operations are already done */ private boolean finished = false; - public CypherInitializer(GraphDatabaseAPI db, Log userLog) { + public CypherInitializer(GraphDatabaseAPI db, Log userLog, + DatabaseManagementService databaseManagementService, + DatabaseEventListeners databaseEventListeners) { this.db = db; this.userLog = userLog; + this.databaseManagementService = databaseManagementService; + this.databaseEventListeners = databaseEventListeners; this.dependencyResolver = db.getDependencyResolver(); this.procs = dependencyResolver.resolveDependency(GlobalProcedures.class); this.defaultDb = dependencyResolver.resolveDependency(Config.class).get(GraphDatabaseSettings.default_database); @@ -142,6 +160,9 @@ public void available() { } } + // create listener for each database + databaseEventListeners.registerDatabaseEventListener(new SystemFunctionalityListener()); + for (String query : initializers) { try { // we need to apply a retry strategy here since in systemdb we potentially conflict with @@ -216,4 +237,37 @@ private boolean areProceduresRegistered(String procStart) { public void unavailable() { // intentionally empty } + + private class SystemFunctionalityListener implements DatabaseEventListener { + + @Override + public void databaseDrop(DatabaseEventContext eventContext) { + + forEachSystemLabel((tx, label) -> { + tx.findNodes(label, database.name(), eventContext.getDatabaseName()) + .forEachRemaining(Node::delete); + }); + } + + @Override + public void databaseStart(DatabaseEventContext eventContext) {} + + @Override + public void databaseShutdown(DatabaseEventContext eventContext) {} + + @Override + public void databasePanic(DatabaseEventContext eventContext) {} + + @Override + public void databaseCreate(DatabaseEventContext eventContext) {} + } + + private void forEachSystemLabel(BiConsumer consumer) { + try (Transaction tx = db.beginTx()) { + for (Label label: SystemLabels.values()) { + consumer.accept(tx, label); + } + tx.commit(); + } + } } diff --git a/core/src/test/java/apoc/trigger/TriggerEnterpriseFeaturesTest.java b/core/src/test/java/apoc/trigger/TriggerEnterpriseFeaturesTest.java index b3b0fd06a9..04093225b6 100644 --- a/core/src/test/java/apoc/trigger/TriggerEnterpriseFeaturesTest.java +++ b/core/src/test/java/apoc/trigger/TriggerEnterpriseFeaturesTest.java @@ -36,12 +36,14 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import static apoc.ApocConfig.APOC_CONFIG_INITIALIZER; import static apoc.ApocConfig.APOC_TRIGGER_ENABLED; import static apoc.trigger.TriggerHandler.TRIGGER_REFRESH; import static apoc.trigger.TriggerTestUtil.TIMEOUT; import static apoc.trigger.TriggerTestUtil.TRIGGER_DEFAULT_REFRESH; import static apoc.util.TestContainerUtil.createEnterpriseDB; import static apoc.util.TestContainerUtil.testCall; +import static apoc.util.TestContainerUtil.testCallEmpty; import static apoc.util.TestContainerUtil.testResult; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -55,6 +57,7 @@ public class TriggerEnterpriseFeaturesTest { private static final String FOO_DB = "foo"; + private static final String INIT_DB = "initdb"; private static final String NO_ADMIN_USER = "nonadmin"; private static final String NO_ADMIN_PWD = "test1234"; @@ -64,10 +67,15 @@ public class TriggerEnterpriseFeaturesTest { @BeforeClass public static void beforeAll() { + final String cypherInitializer = String.format("%s.%s.0", + APOC_CONFIG_INITIALIZER, SYSTEM_DATABASE_NAME); + final String createInitDb = String.format("CREATE DATABASE %s IF NOT EXISTS", INIT_DB); + // We build the project, the artifact will be placed into ./build/libs neo4jContainer = createEnterpriseDB(!TestUtil.isRunningInCI()) .withEnv(APOC_TRIGGER_ENABLED, "true") - .withEnv(TRIGGER_REFRESH, String.valueOf(TRIGGER_DEFAULT_REFRESH)); + .withEnv(TRIGGER_REFRESH, String.valueOf(TRIGGER_DEFAULT_REFRESH)) + .withEnv(cypherInitializer, createInitDb); neo4jContainer.start(); session = neo4jContainer.getSession(); @@ -170,6 +178,49 @@ public void testTriggerInstallInNewDatabase() { } } + @Test + public void testDeleteTriggerAfterDatabaseDeletion() { + try (Session sysSession = neo4jContainer.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + final String dbToDelete = "todelete"; + + // create database with name `todelete` + sysSession.writeTransaction(tx -> tx.run(String.format("CREATE DATABASE %s WAIT;", dbToDelete))); + + testDeleteTriggerAfterDropDb(dbToDelete, sysSession); + } + } + + @Test + public void testDeleteTriggerAfterDatabaseDeletionCreatedViaCypherInit() { + try (Session sysSession = neo4jContainer.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + // the database `initDb` is created via `apoc.initializer.*` + testDeleteTriggerAfterDropDb(INIT_DB, sysSession); + } + } + + private static void testDeleteTriggerAfterDropDb(String dbToDelete, Session sysSession) { + final String defaultTriggerName = UUID.randomUUID().toString(); + + // install and show a trigger in the database and check existence + testCall(sysSession, "CALL apoc.trigger.install($dbName, $name, 'return 1', {})", + Map.of("dbName", dbToDelete, "name", defaultTriggerName), + r -> assertEquals(defaultTriggerName, r.get("name")) + ); + + testCall(sysSession, "CALL apoc.trigger.show($dbName)", + Map.of("dbName", dbToDelete), + r -> assertEquals(defaultTriggerName, r.get("name")) + ); + + // drop database + sysSession.writeTransaction(tx -> tx.run(String.format("DROP DATABASE %s WAIT;", dbToDelete))); + + // check that the trigger has been removed + testCallEmpty(sysSession, "CALL apoc.trigger.show($dbName)", + Map.of("dbName", dbToDelete) + ); + } + @Test public void testTriggersAllowedOnlyWithAdmin() {