diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsSimpleGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsSimpleGoal.java new file mode 100644 index 0000000000..93b5e32aa6 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsSimpleGoal.java @@ -0,0 +1,24 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.ammos.aerie.procedural.timeline.plan.EventQuery; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +@SchedulingProcedure +public record ExternalEventsSimpleGoal() implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + EventQuery eventQuery = new EventQuery("TestGroup", null, null); + + for (final var e: plan.events(eventQuery)) { + plan.create("BiteBanana", new DirectiveStart.Absolute(e.getInterval().start), Map.of("biteSize", SerializedValue.of(1))); + } + plan.commit(); + } +} diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsSourceQueryGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsSourceQueryGoal.java new file mode 100644 index 0000000000..e48739c2a7 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsSourceQueryGoal.java @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.ExternalSource; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.ammos.aerie.procedural.timeline.plan.EventQuery; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +@SchedulingProcedure +public record ExternalEventsSourceQueryGoal() implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + + // extract events belonging to the second source + EventQuery eventQuery = new EventQuery( + null, + null, + List.of(new ExternalSource("NewTest.json", "TestGroup_2")) + ); + + for (final var e: plan.events(eventQuery)) { + // filter events that we schedule off of by key + if (e.key.contains("01")) { + plan.create( + "BiteBanana", + // place the directive such that it is coincident with the event's start + new DirectiveStart.Absolute(e.getInterval().start), + Map.of("biteSize", SerializedValue.of(1))); + } + } + plan.commit(); + } +} diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsTypeQueryGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsTypeQueryGoal.java new file mode 100644 index 0000000000..c6793d8f82 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalEventsTypeQueryGoal.java @@ -0,0 +1,31 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.ammos.aerie.procedural.timeline.plan.EventQuery; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +@SchedulingProcedure +public record ExternalEventsTypeQueryGoal() implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + + // demonstrate more complicated query functionality + EventQuery eventQuery = new EventQuery( + List.of("TestGroup", "TestGroup_2"), + List.of("TestType"), + null + ); + + for (final var e: plan.events(eventQuery)) { + plan.create("BiteBanana", new DirectiveStart.Absolute(e.getInterval().start), Map.of("biteSize", SerializedValue.of(1))); + } + plan.commit(); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsTests.java new file mode 100644 index 0000000000..c1928a9c54 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsTests.java @@ -0,0 +1,234 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; +import gov.nasa.jpl.aerie.e2e.types.Plan; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ExternalEventsTests extends ProceduralSchedulingSetup { + private GoalInvocationId procedureId; + private final static String SOURCE_TYPE = "TestType"; + private final static String EVENT_TYPE = "TestType"; + private final static String ADDITIONAL_EVENT_TYPE = EVENT_TYPE + "_2"; + private final static String DERIVATION_GROUP = "TestGroup"; + private final static String ADDITIONAL_DERIVATION_GROUP = DERIVATION_GROUP + "_2"; + + private final HasuraRequests.ExternalSource externalSource = new HasuraRequests.ExternalSource( + "Test.json", + SOURCE_TYPE, + DERIVATION_GROUP, + "2024-01-01T00:00:00Z", + "2023-01-01T00:00:00Z", + "2023-01-08T00:00:00Z", + "2024-10-01T00:00:00Z" + ); + private final List externalEvents = List.of( + new HasuraRequests.ExternalEvent( + "Event_01", + EVENT_TYPE, + externalSource.key(), + externalSource.derivation_group_name(), + "2023-01-01T01:00:00Z", + "01:00:00" + ), + new HasuraRequests.ExternalEvent( + "Event_02", + EVENT_TYPE, + externalSource.key(), + externalSource.derivation_group_name(), + "2023-01-01T03:00:00Z", + "01:00:00" + ), + new HasuraRequests.ExternalEvent( + "Event_03", + EVENT_TYPE, + externalSource.key(), + externalSource.derivation_group_name(), + "2023-01-01T05:00:00Z", + "01:00:00" + ) + ); + + private final HasuraRequests.ExternalSource additionalExternalSource = new HasuraRequests.ExternalSource( + "NewTest.json", + SOURCE_TYPE, + ADDITIONAL_DERIVATION_GROUP, + "2024-01-01T00:00:00Z", + "2023-01-01T00:00:00Z", + "2023-01-08T00:00:00Z", + "2024-10-01T00:00:00Z" + ); + + private final List additionalExternalEvents = List.of( + new HasuraRequests.ExternalEvent( + "Event_01", + EVENT_TYPE, + additionalExternalSource.key(), + additionalExternalSource.derivation_group_name(), + "2023-01-02T01:00:00Z", + "01:00:00" + ), + new HasuraRequests.ExternalEvent( + "Event_02", + ADDITIONAL_EVENT_TYPE, + additionalExternalSource.key(), + additionalExternalSource.derivation_group_name(), + "2023-01-02T03:00:00Z", + "01:00:00" + ), + new HasuraRequests.ExternalEvent( + "Event_03", + ADDITIONAL_EVENT_TYPE, + additionalExternalSource.key(), + additionalExternalSource.derivation_group_name(), + "2023-01-02T05:00:00Z", + "01:00:00" + ) + ); + + @BeforeEach + void localBeforeEach() throws IOException { + // Upload some External Events (and associated infrastructure) + hasura.insertExternalSourceType(SOURCE_TYPE); + hasura.insertExternalEventType(EVENT_TYPE); + hasura.insertDerivationGroup(DERIVATION_GROUP, SOURCE_TYPE); + hasura.insertExternalSource(externalSource); + hasura.insertExternalEvents(externalEvents); + hasura.insertPlanDerivationGroupAssociation(planId, DERIVATION_GROUP); + + // Upload additional External Events in a different derivation group and of a different type + hasura.insertExternalEventType(ADDITIONAL_EVENT_TYPE); + hasura.insertDerivationGroup(ADDITIONAL_DERIVATION_GROUP, SOURCE_TYPE); + hasura.insertExternalSource(additionalExternalSource); + hasura.insertExternalEvents(additionalExternalEvents); + hasura.insertPlanDerivationGroupAssociation(planId, ADDITIONAL_DERIVATION_GROUP); + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + + // External Event Related + hasura.deletePlanDerivationGroupAssociation(planId, DERIVATION_GROUP); + hasura.deletePlanDerivationGroupAssociation(planId, ADDITIONAL_DERIVATION_GROUP); + hasura.deleteExternalSource(externalSource); + hasura.deleteExternalSource(additionalExternalSource); + hasura.deleteDerivationGroup(DERIVATION_GROUP); + hasura.deleteDerivationGroup(ADDITIONAL_DERIVATION_GROUP); + hasura.deleteExternalSourceType(SOURCE_TYPE); + hasura.deleteExternalEventType(EVENT_TYPE); + hasura.deleteExternalEventType(ADDITIONAL_EVENT_TYPE); + } + + @Test + void testExternalEventSimple() throws IOException { + // first, run the goal + try (final var gateway = new GatewayRequests(playwright)) { + int procedureJarId = gateway.uploadJarFile("build/libs/ExternalEventsSimpleGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + hasura.awaitScheduling(specId); + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + // ensure the order lines up with the events' + activities.sort(Comparator.comparing(Plan.ActivityDirective::startOffset)); + + // compare arrays + assertEquals(externalEvents.size(), activities.size()); + for (int i = 0; i < activities.size(); i++) { + Instant activityStartTime = Duration.addToInstant( + Instant.parse(planStartTimestamp), + Duration.fromString(activities.get(i).startOffset()) + ); + assertEquals(externalEvents.get(i).start_time(), activityStartTime.toString()); + } + } + + @Test + void testExternalEventTypeQuery() throws IOException { + // first, run the goal + try (final var gateway = new GatewayRequests(playwright)) { + int procedureJarId = gateway.uploadJarFile("build/libs/ExternalEventsTypeQueryGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + hasura.awaitScheduling(specId); + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + // ensure the orderings line up + activities.sort(Comparator.comparing(Plan.ActivityDirective::startOffset)); + + // get the set of events we expect (anything in TestGroup or TestGroup_2, and of type TestType) + List expected = new ArrayList<>(); + expected.addAll(externalEvents); + expected.addAll( + additionalExternalEvents.stream() + .filter(e -> e.event_type_name().equals(EVENT_TYPE)) + .toList() + ); + + // explicitly ensure the orderings line up + expected.sort(Comparator.comparing(HasuraRequests.ExternalEvent::start_time)); + + // compare arrays + assertEquals(expected.size(), activities.size()); + for (int i = 0; i < activities.size(); i++) { + Instant activityStartTime = Duration.addToInstant( + Instant.parse(planStartTimestamp), + Duration.fromString(activities.get(i).startOffset()) + ); + assertEquals(activityStartTime.toString(), expected.get(i).start_time()); + } + } + + @Test + void testExternalEventSourceQuery() throws IOException { + // first, run the goal + try (final var gateway = new GatewayRequests(playwright)) { + int procedureJarId = gateway.uploadJarFile("build/libs/ExternalEventsSourceQueryGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + hasura.awaitScheduling(specId); + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + // only 1 activity this time + assertEquals(1, activities.size()); + Instant activityStartTime = Duration.addToInstant( + Instant.parse(planStartTimestamp), + Duration.fromString(activities.get(0).startOffset()) + ); + assertEquals(activityStartTime.toString(), additionalExternalEvents.get(0).start_time()); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 3de72613b4..8d07e3fa95 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -82,6 +82,38 @@ mutation CreateActivityDirective($activityDirectiveInsertInput: activity_directi id } }"""), + CREATE_EXTERNAL_EVENT_TYPE(""" + mutation CreateExternalEventType($eventType: external_event_type_insert_input!) { + createExternalEventType: insert_external_event_type_one(object: $eventType) { + name + } + }"""), + CREATE_EXTERNAL_EVENTS(""" + mutation InsertExternalEvents($objects: [external_event_insert_input!]!) { + insertExternalEvents: insert_external_event(objects: $objects) { + returning { + key + } + } + }"""), + CREATE_EXTERNAL_SOURCE(""" + mutation InsertExternalSource($object: external_source_insert_input!) { + insertExternalSource: insert_external_source_one(object: $object) { + key + } + }"""), + CREATE_EXTERNAL_SOURCE_TYPE(""" + mutation CreateExternalSourceType($sourceType: external_source_type_insert_input!) { + createExternalSourceType: insert_external_source_type_one(object: $sourceType) { + name + } + }"""), + CREATE_DERIVATION_GROUP(""" + mutation CreateDerivationGroup($derivationGroup: derivation_group_insert_input!) { + createDerivationGroup: insert_derivation_group_one(object: $derivationGroup) { + name + } + }"""), CREATE_MISSION_MODEL(""" mutation CreateMissionModel($model: mission_model_insert_input!) { insert_mission_model_one(object: $model) { @@ -95,6 +127,12 @@ mutation CreatePlan($plan: plan_insert_input!) { revision } }"""), + CREATE_PLAN_DERIVATION_GROUP(""" + mutation CreatePlanDerivationGroup($source: plan_derivation_group_insert_input!) { + planExternalSourceLink: insert_plan_derivation_group_one(object: $source) { + derivation_group_name + } + }"""), CREATE_SCHEDULING_SPEC_GOAL(""" mutation CreateSchedulingSpecGoal($spec_goal: scheduling_specification_goals_insert_input!) { insert_scheduling_specification_goals_one(object: $spec_goal) { @@ -140,12 +178,38 @@ mutation DeleteConstraint($id: Int!) { id } }"""), + DELETE_DERIVATION_GROUP(""" + mutation DeleteDerivationGroup($name: String!) { + deleteDerivationGroup: delete_derivation_group(where: { name: { _eq: $name } }) { + returning { + name + } + } + }"""), DELETE_EXTERNAL_DATASET(""" mutation deleteExtProfile($plan_id: Int!, $dataset_id: Int!) { delete_plan_dataset_by_pk(plan_id:$plan_id, dataset_id:$dataset_id) { dataset_id } }"""), + DELETE_EXTERNAL_EVENT_TYPE(""" + mutation DeleteExternalEventType($name: String!) { + deleteExternalEventType: delete_external_event_type_by_pk(name: $name) { + name + } + }"""), + DELETE_EXTERNAL_SOURCE(""" + mutation DeleteExternalSource($derivationGroupName: String!, $sourceKey: String!) { + deleteExternalSource: delete_external_source_by_pk(derivation_group_name: $derivationGroupName, key: $sourceKey) { + key + } + }"""), + DELETE_EXTERNAL_SOURCE_TYPE(""" + mutation DeleteExternalSourceType($name: String!) { + deleteExternalSourceType: delete_external_source_type_by_pk(name: $name) { + name + } + }"""), DELETE_MISSION_MODEL(""" mutation DeleteModel($id: Int!) { delete_mission_model_by_pk(id: $id) { @@ -174,6 +238,12 @@ mutation DeletePlan($id: Int!) { } } }"""), + DELETE_PLAN_DERIVATION_GROUP(""" + mutation DeletePlanExternalSource($derivationGroupName: String!, $planId: Int!) { + planDerivationGroupLink: delete_plan_derivation_group_by_pk(derivation_group_name: $derivationGroupName, plan_id: $planId) { + derivation_group_name + } + }"""), DELETE_SCHEDULING_GOAL(""" mutation DeleteSchedulingGoal($goalId: Int!) { delete_scheduling_specification_goals(where: {goal_id: {_eq: $goalId}}){ diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index ccb55dbe50..71b5dff661 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -10,6 +10,7 @@ import javax.json.Json; import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; import javax.json.JsonValue; import javax.json.JsonObject; import java.io.IOException; @@ -98,6 +99,11 @@ private JsonObject makeRequest( } } + //region Records + public record ExternalEvent(String key, String event_type_name, String source_key, String derivation_group_name, String start_time, String duration) {} + public record ExternalSource(String key, String source_type_name, String derivation_group_name, String valid_at, String start_time, String end_time, String created_at){} + //endregion Records + //region Mission Model public int createMissionModel(int jarId, String name, String mission, String version) throws IOException, InterruptedException @@ -988,6 +994,153 @@ public void deleteExternalDataset(int planId, int datasetId) throws IOException } //endregion + // region External Events + public String insertExternalSourceType( + String name + ) throws IOException { + final var insertExternalSourceTypeBuilder = Json.createObjectBuilder() + .add("name", name) + .build(); + final var variables = Json.createObjectBuilder().add("sourceType", insertExternalSourceTypeBuilder).build(); + return makeRequest(GQL.CREATE_EXTERNAL_SOURCE_TYPE, variables) + .getJsonObject("createExternalSourceType") + .getString("name"); + } + public String insertExternalEventType( + String name + ) throws IOException { + final var insertExternalSourceTypeBuilder = Json.createObjectBuilder() + .add("name", name) + .build(); + final var variables = Json.createObjectBuilder().add("eventType", insertExternalSourceTypeBuilder).build(); + return makeRequest(GQL.CREATE_EXTERNAL_EVENT_TYPE, variables) + .getJsonObject("createExternalEventType") + .getString("name"); + } + public String insertDerivationGroup( + String name, + String sourceTypeName + ) throws IOException { + final var insertDerivationGroupBuilder = Json.createObjectBuilder() + .add("name", name) + .add("source_type_name", sourceTypeName) + .build(); + final var variables = Json.createObjectBuilder().add("derivationGroup", insertDerivationGroupBuilder).build(); + return makeRequest(GQL.CREATE_DERIVATION_GROUP, variables) + .getJsonObject("createDerivationGroup") + .getString("name"); + } + public String insertExternalSource( + ExternalSource externalSource + ) throws IOException { + final var insertExternalSourceBuilder = Json.createObjectBuilder() + .add("key", externalSource.key()) + .add("source_type_name", externalSource.source_type_name()) + .add("derivation_group_name", externalSource.derivation_group_name()) + .add("valid_at", externalSource.valid_at()) + .add("start_time", externalSource.start_time()) + .add("end_time", externalSource.end_time()) + .add("created_at", externalSource.created_at()) + .build(); + final var variables = Json.createObjectBuilder().add("object", insertExternalSourceBuilder).build(); + return makeRequest(GQL.CREATE_EXTERNAL_SOURCE, variables) + .getJsonObject("insertExternalSource") + .getString("key"); + } + public JsonArray insertExternalEvents( + List externalEvents + ) throws IOException { + JsonArrayBuilder formattedEvents = Json.createArrayBuilder(); + for (ExternalEvent e : externalEvents) { + formattedEvents.add( + Json.createObjectBuilder() + .add("key", e.key()) + .add("event_type_name", e.event_type_name()) + .add("source_key", e.source_key()) + .add("derivation_group_name", e.derivation_group_name()) + .add("start_time", e.start_time()) + .add("duration", e.duration()) + .build() + ); + } + final var variables = Json.createObjectBuilder() + .add("objects", formattedEvents.build()) + .build(); + return makeRequest(GQL.CREATE_EXTERNAL_EVENTS, variables) + .getJsonObject("insertExternalEvents") + .getJsonArray("returning"); + } + public String insertPlanDerivationGroupAssociation( + int planId, + String derivationGroupName + ) throws IOException { + final var insertPlanDerivationGroupBuilder = Json.createObjectBuilder() + .add("plan_id", planId) + .add("derivation_group_name", derivationGroupName) + .build(); + final var variables = Json.createObjectBuilder().add("source", insertPlanDerivationGroupBuilder).build(); + return makeRequest(GQL.CREATE_PLAN_DERIVATION_GROUP, variables) + .getJsonObject("planExternalSourceLink") + .getString("derivation_group_name"); + } + + public String deleteExternalSourceType( + String name + ) throws IOException { + final var variables = Json.createObjectBuilder() + .add("name", name) + .build(); + return makeRequest(GQL.DELETE_EXTERNAL_SOURCE_TYPE, variables) + .getJsonObject("deleteExternalSourceType") + .getString("name"); + } + public String deleteExternalEventType( + String name + ) throws IOException { + final var variables = Json.createObjectBuilder() + .add("name", name) + .build(); + return makeRequest(GQL.DELETE_EXTERNAL_EVENT_TYPE, variables) + .getJsonObject("deleteExternalEventType") + .getString("name"); + } + public JsonArray deleteDerivationGroup( + String name + ) throws IOException { + final var variables = Json.createObjectBuilder() + .add("name", name) + .build(); + return makeRequest(GQL.DELETE_DERIVATION_GROUP, variables) + .getJsonObject("deleteDerivationGroup") + .getJsonArray("returning"); + } + public String deleteExternalSource( + ExternalSource externalSource + ) throws IOException { + final var variables = Json.createObjectBuilder() + .add("sourceKey", externalSource.key()) + .add("derivationGroupName", externalSource.derivation_group_name()) + .build(); + // NOTE: this deletes external events as well, as deletions of sources cascade to their contained events. + return makeRequest(GQL.DELETE_EXTERNAL_SOURCE, variables) + .getJsonObject("deleteExternalSource") + .getString("key"); + } + public String deletePlanDerivationGroupAssociation( + int planId, + String derivationGroupName + ) throws IOException { + final var variables = Json.createObjectBuilder() + .add("planId", planId) + .add("derivationGroupName", derivationGroupName) + .build(); + return makeRequest(GQL.DELETE_PLAN_DERIVATION_GROUP, variables) + .getJsonObject("planDerivationGroupLink") + .getString("derivation_group_name"); + } + + // endregion + //region Constraints public List checkConstraints(int planID) throws IOException { final var variables = Json.createObjectBuilder()