diff --git a/src/prerna/collaboration/WorkRoutineRoom.java b/src/prerna/collaboration/WorkRoutineRoom.java index d91b8091110..02a6d95a61d 100644 --- a/src/prerna/collaboration/WorkRoutineRoom.java +++ b/src/prerna/collaboration/WorkRoutineRoom.java @@ -49,7 +49,10 @@ import prerna.reactor.automation.run.AutomationRunStore; import prerna.reactor.automation.utils.AutomationRuntimeUtils; -/** Opens a private discussion of one completed routine output without exposing its automation history. */ +/** + * The owner's routine runs: their history, the latest runs for the sidebar, deleting one, and a private discussion of + * one completed output, without exposing the automation history behind them. + */ public final class WorkRoutineRoom { private static final int MAX_RUNS = 30; // the sidebar shows the routines that ran last, each with its latest runs @@ -206,14 +209,37 @@ private static String roomOf(String runId) { return null; } + /** + * Deletes one finished run of the owner's routine from its history. When it was the routine's latest run, Routines + * stops showing its result too. Chats opened from the run stay; they are the owner's own conversations. + */ + public static void deleteRun(Insight insight, String routineId, String runId) { + WorkRoutine.Slot slot = target(insight, routineId).slot(); + String id = runId == null ? "" : runId.trim(); + Map run = id.isEmpty() ? null : ownRun(slot, insight.getUser(), id); + if (run == null) { + throw new IllegalArgumentException("Unknown run"); + } + if ("running".equals(status(run))) { + throw new IllegalArgumentException("This run is still going. Delete it once it has finished."); + } + AutomationRunStore.deleteRun(AutomationProjectService.inlineRunScope(insight.getUser()), slot.automationId(), id); + // the row keeps its start, which orders the sidebar, and the older runs stay in the history + CollaborationDbUtils.update("UPDATE " + slot.kind().table() + " SET RUN_STATUS = NULL, FINISHED_AT = NULL, " + + "ROOM_ID = NULL, RUN_ID = NULL, SUMMARY = NULL, ERROR = NULL, ERROR_CAUSE = NULL" + slot.where() + + " AND RUN_ID = ?", slot.args(new Object[0], id)); + } + + /** The run, only when it is this owner's run of this routine; the client id is never trusted alone. */ + private static Map ownRun(WorkRoutine.Slot slot, User user, String runId) { + Map run = AutomationRunStore.getRunDetail(runId); + return run != null && AutomationProjectService.inlineRunScope(user).equals(run.get(AutomationConstants.PROJECT_ID)) + && slot.automationId().equals(run.get(AutomationConstants.AUTOMATION_ID)) ? run : null; + } + /** One run's result text, only when the run is this owner's run of this routine; null when it has none. */ private static String runOutput(WorkRoutine.Slot slot, User user, String runId) { - Map run = AutomationRunStore.getRunDetail(runId); - if (run == null || !AutomationProjectService.inlineRunScope(user).equals(run.get(AutomationConstants.PROJECT_ID)) - || !slot.automationId().equals(run.get(AutomationConstants.AUTOMATION_ID))) { - return null; - } - return output(slot, runId); + return ownRun(slot, user, runId) == null ? null : output(slot, runId); } private static String output(WorkRoutine.Slot slot, String runId) { diff --git a/src/prerna/reactor/automation/run/AutomationRunStore.java b/src/prerna/reactor/automation/run/AutomationRunStore.java index 1906d0e39e9..72c2a528e47 100644 --- a/src/prerna/reactor/automation/run/AutomationRunStore.java +++ b/src/prerna/reactor/automation/run/AutomationRunStore.java @@ -272,16 +272,22 @@ private AutomationRunStore() { PARENT_NODE_ID, ITERATION_INDEX, SOURCE_NODE_ID) \ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""; - // Child rows first, then the runs, all selected by run scope and automation. + // Child rows first, then the runs, all selected by run scope and automation, and for one run its id too. private static final String RUNS_IN_SCOPE = "SELECT RUN_ID FROM AUTOMATION_RUNS WHERE PROJECT_ID = ? AND AUTOMATION_ID = ?"; - private static final List DELETE_RUNS = List.of( - "DELETE FROM AUTOMATION_RESULT_CHUNKS WHERE RESULT_ID IN (SELECT RESULT_ID FROM AUTOMATION_SAVED_RESULTS " - + "WHERE RUN_ID IN (" + RUNS_IN_SCOPE + "))", - "DELETE FROM AUTOMATION_SAVED_RESULTS WHERE RUN_ID IN (" + RUNS_IN_SCOPE + ")", - "DELETE FROM AUTOMATION_RUN_NODE_SOURCES WHERE RUN_ID IN (" + RUNS_IN_SCOPE + ")", - "DELETE FROM AUTOMATION_NODE_OUTPUTS WHERE RUN_ID IN (" + RUNS_IN_SCOPE + ")", - "DELETE FROM AUTOMATION_RUN_WAITS WHERE RUN_ID IN (" + RUNS_IN_SCOPE + ")", - "DELETE FROM AUTOMATION_RUNS WHERE PROJECT_ID = ? AND AUTOMATION_ID = ?"); + private static final List DELETE_RUNS = deleteStatements(""); + private static final List DELETE_RUN = deleteStatements(" AND RUN_ID = ?"); + + private static List deleteStatements(String andRun) { + String runs = RUNS_IN_SCOPE + andRun; + return List.of( + "DELETE FROM AUTOMATION_RESULT_CHUNKS WHERE RESULT_ID IN (SELECT RESULT_ID FROM AUTOMATION_SAVED_RESULTS " + + "WHERE RUN_ID IN (" + runs + "))", + "DELETE FROM AUTOMATION_SAVED_RESULTS WHERE RUN_ID IN (" + runs + ")", + "DELETE FROM AUTOMATION_RUN_NODE_SOURCES WHERE RUN_ID IN (" + runs + ")", + "DELETE FROM AUTOMATION_NODE_OUTPUTS WHERE RUN_ID IN (" + runs + ")", + "DELETE FROM AUTOMATION_RUN_WAITS WHERE RUN_ID IN (" + runs + ")", + "DELETE FROM AUTOMATION_RUNS WHERE PROJECT_ID = ? AND AUTOMATION_ID = ?" + andRun); + } private static final String INCREMENT_RUN_TOTAL_NODES = "UPDATE AUTOMATION_RUNS SET TOTAL_NODES = TOTAL_NODES + ? WHERE RUN_ID = ?"; @@ -1135,6 +1141,24 @@ public static List> getRunsForAutomation(String runScope, St * @throws IllegalStateException when the rows cannot be deleted */ public static int deleteRuns(String runScope, String automationId) { + return delete(DELETE_RUNS, runScope, automationId, null); + } + + /** + * Deletes one run of one automation in one run scope, with its source snapshot, node outputs, and waits, in one + * transaction. A run id from another scope or automation deletes nothing. + * + * @param runScope Automation project ID or inline run scope + * @param automationId logical automation stored in {@code AUTOMATION_ID} + * @param runId the run + * @return 1 when the run was deleted, else 0; 0 when the scheduler DB is unavailable + * @throws IllegalStateException when the rows cannot be deleted + */ + public static int deleteRun(String runScope, String automationId, String runId) { + return delete(DELETE_RUN, runScope, automationId, runId); + } + + private static int delete(List statements, String runScope, String automationId, String runId) { IRDBMSEngine schedulerDb = getSchedulerDb(); if (schedulerDb == null) { return 0; @@ -1142,10 +1166,13 @@ public static int deleteRuns(String runScope, String automationId) { try { return QueryExecutionUtility.write(schedulerDb, conn -> { int deleted = 0; - for (String sql : DELETE_RUNS) { + for (String sql : statements) { deleted = QueryExecutionUtility.executeUpdate(conn, sql, ps -> { ps.setString(1, runScope); ps.setString(2, automationId); + if (runId != null) { + ps.setString(3, runId); + } }); } // The last statement deletes the runs themselves. diff --git a/src/prerna/reactor/collaboration/WorkDeleteRoutineRunReactor.java b/src/prerna/reactor/collaboration/WorkDeleteRoutineRunReactor.java new file mode 100644 index 00000000000..45ea73263dd --- /dev/null +++ b/src/prerna/reactor/collaboration/WorkDeleteRoutineRunReactor.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.collaboration; + +import java.util.Map; + +import prerna.collaboration.WorkRoutineRoom; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +public class WorkDeleteRoutineRunReactor extends AbstractCollaborationReactor { + private static final String ROUTINE_ID = "routineId"; + private static final String RUN_ID = "runId"; + + public WorkDeleteRoutineRunReactor() { + this.keysToGet = new String[] { ROUTINE_ID, RUN_ID }; + this.keyRequired = new int[] { 1, 1 }; + } + + @Override + public NounMetadata execute() { + getUser(); + WorkRoutineRoom.deleteRun(this.insight, getString(ROUTINE_ID), getString(RUN_ID)); + return mapResult(Map.of("deleted", true)); + } + + @Override + public String getReactorDescription() { + return "Deletes one finished run of one of the signed-in user's routines from its run history; when it was the " + + "routine's latest run, Routines stops showing its result. Chats opened from the run are kept"; + } + + @Override + protected String getDescriptionForKey(String key) { + if (ROUTINE_ID.equals(key)) { + return "Routine id: inbox-daily-brief, weekly-meeting-agendas, event-planning, expense-review, or the id of " + + "one of the owner's own routines"; + } + if (RUN_ID.equals(key)) { + return "The run to delete, from the routine's run history"; + } + return super.getDescriptionForKey(key); + } +} diff --git a/test/prerna/reactor/automation/run/AutomationRunStoreUnitTests.java b/test/prerna/reactor/automation/run/AutomationRunStoreUnitTests.java index f3375dd3c88..bd708cc5317 100644 --- a/test/prerna/reactor/automation/run/AutomationRunStoreUnitTests.java +++ b/test/prerna/reactor/automation/run/AutomationRunStoreUnitTests.java @@ -284,6 +284,41 @@ void inlineScopeRunsCompleteAndDeleteOnlyTheirOwnRows() throws Exception { } } + @Test + void deletingOneRunLeavesTheAutomationsOtherRuns() throws Exception { + try (var db = new JdbcTestDatabase()) { + schema(db); + savedResultSchema(db); + db.execute( + "CREATE TABLE AUTOMATION_RUN_WAITS (WAIT_ID VARCHAR, RUN_ID VARCHAR, NODE_ID VARCHAR, WAIT_TYPE VARCHAR, AGENT_RUN_ID VARCHAR, ROOM_ID VARCHAR, RESUME_NODE_ID VARCHAR, STATUS VARCHAR, CREATED_BY VARCHAR, STARTED_AT TIMESTAMP, EXPIRES_AT TIMESTAMP, RESOLVED_AT TIMESTAMP, RESOLVED_BY VARCHAR)"); + String scope = AutomationConstants.INLINE_RUN_SCOPE_PREFIX + "owner"; + List> nodes = List.of(Map.of("id", "node", "label", "Node")); + AutomationRunStore.initializeRun("old", scope, "brief", 1, "hash", "{}", Map.of(), "SCHEDULED", "user", + nodes, Map.of(), Map.of("node", "source")); + AutomationRunStore.initializeRun("new", scope, "brief", 1, "hash", "{}", Map.of(), "SCHEDULED", "user", + nodes, Map.of(), Map.of("node", "source")); + db.execute("INSERT INTO AUTOMATION_RUN_WAITS (WAIT_ID, RUN_ID, NODE_ID, STATUS) " + + "VALUES ('wait', 'new', 'node', 'RESOLVED')", + "INSERT INTO AUTOMATION_SAVED_RESULTS (RESULT_ID, RUN_ID) VALUES ('result', 'new')", + "INSERT INTO AUTOMATION_RESULT_CHUNKS (RESULT_ID, CHUNK_INDEX) VALUES ('result', 0)"); + + // the run id alone is not enough: another scope or automation deletes nothing + assertEquals(0, AutomationRunStore.deleteRun(AutomationConstants.INLINE_RUN_SCOPE_PREFIX + "other", + "brief", "new")); + assertEquals(0, AutomationRunStore.deleteRun(scope, "default", "new")); + assertEquals(1, AutomationRunStore.deleteRun(scope, "brief", "new")); + + assertEquals(1, db.count("AUTOMATION_RUNS")); + assertEquals("old", db.value("SELECT RUN_ID FROM AUTOMATION_RUNS")); + assertEquals(1, db.count("AUTOMATION_RUN_NODE_SOURCES")); + assertEquals(1, db.count("AUTOMATION_NODE_OUTPUTS")); + assertEquals(0, db.count("AUTOMATION_RUN_WAITS")); + assertEquals(0, db.count("AUTOMATION_SAVED_RESULTS")); + assertEquals(0, db.count("AUTOMATION_RESULT_CHUNKS")); + assertEquals(0, AutomationRunStore.deleteRun(scope, "brief", "new")); + } + } + @Test void lookAlikeScopeGetsNoRunsFromADatabaseThatIgnoresCase() throws Exception { String scope = AutomationConstants.INLINE_RUN_SCOPE_PREFIX + "owner";