diff --git a/.circleci/RoboFile.php b/.circleci/RoboFile.php
index 68ccf731..fbacafae 100644
--- a/.circleci/RoboFile.php
+++ b/.circleci/RoboFile.php
@@ -411,6 +411,9 @@ public function configureModuleDependencies()
unset($config->require->{"drupal/core"});
$config->require->{"drupal/core-recommended"} = "~8.8";
+ // Add rules for testing apigee_edge_actions.
+ $config->require->{"drupal/rules"} = "^3.0@alpha";
+
// We require Drupal console and drush for some tests.
$config->require->{"drupal/console"} = "~1.0";
$config->require->{"drush/drush"} = "^9.7";
diff --git a/modules/apigee_edge_actions/README.md b/modules/apigee_edge_actions/README.md
new file mode 100644
index 00000000..8098c048
--- /dev/null
+++ b/modules/apigee_edge_actions/README.md
@@ -0,0 +1,53 @@
+# Apigee Edge Actions
+
+The Apigee Edge Actions module provides rules integration for Apigee Edge. It makes it easy to automate tasks and react on events such as:
+
+ * Sending an email when an App is created.
+ * Notify a developer when added to a Team.
+ * Notify admin when an API product is added to an App.
+
+## Events
+
+The following events are supported out of the box:
+
+### App
+`\Drupal\apigee_edge\Entity\DeveloperApp`
+
+| Event | Name |
+|---|---|
+| After saving a new App | `apigee_edge_actions_entity_insert:developer_app` |
+| After deleting an App | `apigee_edge_actions_entity_delete:developer_app` |
+| After updating an App | `apigee_edge_actions_entity_insert:developer_app` |
+| After adding an API Product | `apigee_edge_actions_entity_add_product:developer_app` |
+| After removing an API Product | `apigee_edge_actions_entity_remove_product:developer_app` |
+
+### Team App
+`\Drupal\apigee_edge_teams\Entity\TeamApp`
+
+| Event | Name |
+|---|---|
+| After saving a new Team App | `apigee_edge_actions_entity_insert:team_app` |
+| After deleting an Team App | `apigee_edge_actions_entity_delete:team_app` |
+| After updating an Team App | `apigee_edge_actions_entity_insert:team_app` |
+| After adding an API Product | `apigee_edge_actions_entity_add_product:team_app` |
+| After removing an API Product | `apigee_edge_actions_entity_remove_product:team_app` |
+
+### Team
+`\Drupal\apigee_edge_teams\Entity\Team`
+
+| Event | Name |
+|---|---|
+| After saving a new Team | `apigee_edge_actions_entity_insert:team` |
+| After deleting an Team | `apigee_edge_actions_entity_delete:team` |
+| After updating an Team | `apigee_edge_actions_entity_insert:team` |
+| After adding a team member | `apigee_edge_actions_entity_add_member:team` |
+| After removing a team member | `apigee_edge_actions_entity_remove_member:team` |
+
+## Examples
+
+The `apigee_edge_actions_examples` module ships with some example rules you can use to test:
+
+1. Log a message when team is deleted.
+2. Notify developer when added to a team
+3. Notify developer when adding a new app
+4. Notify site admins when app is created
diff --git a/modules/apigee_edge_actions/apigee_edge_actions.info.yml b/modules/apigee_edge_actions/apigee_edge_actions.info.yml
new file mode 100644
index 00000000..cdfc32b3
--- /dev/null
+++ b/modules/apigee_edge_actions/apigee_edge_actions.info.yml
@@ -0,0 +1,9 @@
+name: Apigee Edge Actions
+description: Rules integration for Apigee Edge.
+package: Apigee (Experimental)
+type: module
+core: 8.x
+configure: entity.rules_reaction_rule.collection
+dependencies:
+ - apigee_edge:apigee_edge
+ - rules:rules
diff --git a/modules/apigee_edge_actions/apigee_edge_actions.module b/modules/apigee_edge_actions/apigee_edge_actions.module
new file mode 100644
index 00000000..856ac3a7
--- /dev/null
+++ b/modules/apigee_edge_actions/apigee_edge_actions.module
@@ -0,0 +1,120 @@
+isFieldableEdgeEntityType($entity->getEntityType())) {
+ return;
+ }
+
+ $dispatched_event_name = "apigee_edge_actions_entity_$event_name:{$entity->getEntityTypeId()}";
+
+ $arguments = [
+ $entity->getEntityTypeId() => $entity
+ ];
+
+ // Note: Refactor this to plugins if more entity types requires custom
+ // arguments.
+ if ($entity instanceof AppInterface) {
+ if ($entity instanceof DeveloperAppInterface) {
+ // $entity->getCreatedBy() is deprecated, so to get the developer Drupal
+ // account we need to load the developer by UUID, then load the user by
+ // email.
+ // Note: $entity->getAppOwner() returns a developer UUID, which is
+ // different from a user's UUID, so we load the developer first and then
+ // the account.
+ $developer = Drupal::entityTypeManager()
+ ->getStorage('developer')
+ ->load($entity->getAppOwner());
+ $user_id = $developer->getEmail();
+ }
+ else {
+ /** @var \Drupal\apigee_edge_teams\Entity\TeamAppInterface $entity */
+ // For TeamApps, getAppOwner() is a team name, not a developer or email,
+ // and we cannot rely on getCreatedBy() as it is deprecated, so we
+ // default to the current user for the developer.
+ $user_id = Drupal::currentUser()->getEmail();
+
+ // Add the team.
+ $team = Drupal::entityTypeManager()
+ ->getStorage('team')
+ ->load($entity->getAppOwner());
+ $arguments['team'] = $team;
+ }
+
+ // Add the developer.
+ $arguments['developer'] = user_load_by_mail($user_id);
+ }
+
+ if ($event_name === 'update') {
+ $arguments["{$entity->getEntityTypeId()}_unchanged"] = $entity->original;
+ }
+
+ /** @var \Drupal\apigee_edge\Entity\EdgeEntityInterface $entity */
+ Drupal::service('event_dispatcher')
+ ->dispatch($dispatched_event_name, new EdgeEntityEventEdge($entity, $arguments));
+}
diff --git a/modules/apigee_edge_actions/apigee_edge_actions.rules.events.yml b/modules/apigee_edge_actions/apigee_edge_actions.rules.events.yml
new file mode 100644
index 00000000..3c7b37a5
--- /dev/null
+++ b/modules/apigee_edge_actions/apigee_edge_actions.rules.events.yml
@@ -0,0 +1,27 @@
+apigee_edge_actions_entity_insert:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityInsertEventDeriver'
+ event: 'insert'
+
+apigee_edge_actions_entity_delete:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityDeleteEventDeriver'
+ event: 'delete'
+
+apigee_edge_actions_entity_update:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityUpdateEventDeriver'
+ event: 'update'
+
+apigee_edge_actions_entity_add_member:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityAddMemberEventDeriver'
+ event: 'add_member'
+
+apigee_edge_actions_entity_remove_member:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityRemoveMemberEventDeriver'
+ event: 'remove_member'
+
+apigee_edge_actions_entity_add_product:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityAddProductEventDeriver'
+ event: 'add_product'
+
+apigee_edge_actions_entity_remove_product:
+ deriver: '\Drupal\apigee_edge_actions\Plugin\RulesEvent\EdgeEntityRemoveProductEventDeriver'
+ event: 'remove_product'
diff --git a/modules/apigee_edge_actions/apigee_edge_actions.services.yml b/modules/apigee_edge_actions/apigee_edge_actions.services.yml
new file mode 100644
index 00000000..070d4d83
--- /dev/null
+++ b/modules/apigee_edge_actions/apigee_edge_actions.services.yml
@@ -0,0 +1,13 @@
+services:
+ apigee_edge_actions.edge_entity_type_manager:
+ class: Drupal\apigee_edge_actions\ApigeeActionsEntityTypeHelper
+ arguments: ['@entity_type.manager']
+ logger.channel.apigee_edge_actions:
+ parent: logger.channel_base
+ arguments: ['apigee_edge_actions']
+ apigee_edge_actions.events_subscriber:
+ class: Drupal\apigee_edge_actions\EventSubscriber\AppCredentialEventSubscriber
+ arguments:
+ ['@entity_type.manager', '@event_dispatcher', '@current_user', '@logger.channel.apigee_edge_actions']
+ tags:
+ - { name: 'event_subscriber' }
diff --git a/modules/apigee_edge_actions/apigee_edge_actions.tokens.inc b/modules/apigee_edge_actions/apigee_edge_actions.tokens.inc
new file mode 100644
index 00000000..a5e70643
--- /dev/null
+++ b/modules/apigee_edge_actions/apigee_edge_actions.tokens.inc
@@ -0,0 +1,98 @@
+getEntityTypes();
+ $type_info = Drupal::service('plugin.manager.field.field_type')->getDefinitions();
+
+ foreach ($apigee_entity_types as $entity_type) {
+ $token_type = $entity_type->get('token_type');
+
+ if (!isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
+ $info['types'][$entity_type->id()] = [
+ 'name' => $entity_type->getLabel(),
+ 'needs-data' => $entity_type->id(),
+ 'description' => t('Tokens related to @name.', [
+ '@name' => $entity_type->getPluralLabel(),
+ ]),
+ 'module' => 'apigee_edge_actions',
+ ];
+
+ $fields = Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type->id());
+ foreach ($fields as $field_name => $field) {
+ /** @var \Drupal\field\FieldStorageConfigInterface $field */
+ $params['@type'] = $type_info[$field->getType()]['label'];
+ $description = t('@type field.', $params);
+
+ $labels = _token_field_label($entity_type->id(), $field->getName());
+ $label = array_shift($labels);
+ if (!empty($labels)) {
+ $params['%labels'] = implode(', ', $labels);
+ $description = t('@type field. Also known as %labels.', $params);
+ }
+
+ $info['tokens'][$token_type][$field_name] = [
+ 'name' => Html::escape($label),
+ 'description' => $description,
+ 'module' => 'token',
+ ];
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function apigee_edge_actions_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+ $replacements = [];
+
+ if ($type == 'entity' && !empty($data['entity_type']) && !empty($data['entity']) && !empty($data['token_type'])) {
+ /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $entity = $data['entity'];
+
+ if ($entity instanceof EdgeEntityInterface) {
+ foreach ($tokens as $field_name => $original) {
+ // Ensure entity has requested field and is not empty.
+ if (!$entity->hasField($field_name) || $entity->get($field_name)->isEmpty()) {
+ continue;
+ }
+
+ $replacements[$original] = $entity->get($field_name)->value;
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/modules/apigee_edge_actions/composer.json b/modules/apigee_edge_actions/composer.json
new file mode 100644
index 00000000..1b515a52
--- /dev/null
+++ b/modules/apigee_edge_actions/composer.json
@@ -0,0 +1,16 @@
+{
+ "name": "drupal/apigee_edge_actions",
+ "description": "Rules integration for Apigee Edge.",
+ "type": "drupal-module",
+ "license": "GPL-2.0-or-later",
+ "require": {
+ "php": ">=7.1",
+ "drupal/apigee_edge": "*",
+ "drupal/rules": "^3.0@alpha"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/apigee_edge_actions_debug.info.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/apigee_edge_actions_debug.info.yml
new file mode 100644
index 00000000..f4dcbee3
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/apigee_edge_actions_debug.info.yml
@@ -0,0 +1,7 @@
+name: Apigee Edge Actions Debug
+description: Logs debug information for Apigee Edge Actions.
+package: Apigee (Experimental)
+type: module
+core: 8.x
+dependencies:
+ - apigee_edge_actions:apigee_edge_actions
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/apigee_edge_actions_debug.services.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/apigee_edge_actions_debug.services.yml
new file mode 100644
index 00000000..4fcca814
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/apigee_edge_actions_debug.services.yml
@@ -0,0 +1,7 @@
+services:
+ apigee_edge_actions_debug_subscriber:
+ class: 'Drupal\apigee_edge_actions_debug\EventSubscriber\ApigeeEdgeActionsDebugEventSubscriber'
+ arguments:
+ ['@logger.channel.apigee_edge_actions']
+ tags:
+ - { name: 'event_subscriber' }
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/src/EventSubscriber/ApigeeEdgeActionsDebugEventSubscriber.php b/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/src/EventSubscriber/ApigeeEdgeActionsDebugEventSubscriber.php
new file mode 100644
index 00000000..aa466470
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_debug/src/EventSubscriber/ApigeeEdgeActionsDebugEventSubscriber.php
@@ -0,0 +1,86 @@
+logger = $logger;
+ }
+
+ /**
+ * Responds to rules events.
+ *
+ * @param \Symfony\Component\EventDispatcher\Event $event
+ * The event object.
+ * @param string $event_name
+ * The event name.
+ */
+ public function onRulesEvent(Event $event, $event_name) {
+ // Log the dispatched event.
+ if ($event instanceof ApigeeEdgeActionsEventInterface) {
+ $this->logger->notice("Event $event_name was dispatched.");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // Ensure this is called after the container is built.
+ if (!\Drupal::hasService('state')) {
+ return [];
+ }
+
+ $events = [];
+
+ // Register a callback for all registered rules events.
+ if ($rules_events = \Drupal::state()->get('rules.registered_events')) {
+ foreach ($rules_events as $rules_event) {
+ $events[$rules_event][] = ['onRulesEvent', 100];
+ }
+ }
+
+ return $events;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/apigee_edge_actions_examples.info.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/apigee_edge_actions_examples.info.yml
new file mode 100644
index 00000000..19666cbe
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/apigee_edge_actions_examples.info.yml
@@ -0,0 +1,11 @@
+name: Apigee Edge Actions Examples
+description: Example rules for Apigee Edge.
+package: Apigee (Experimental)
+type: module
+core: 8.x
+configure: entity.rules_reaction_rule.collection
+dependencies:
+ - apigee_edge_actions:apigee_edge_actions
+ - apigee_edge:apigee_edge
+ - apigee_edge_teams:apigee_edge_teams
+ - rules:rules
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.log_a_message_when_team_is_deleted.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.log_a_message_when_team_is_deleted.yml
new file mode 100644
index 00000000..01fff4e4
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.log_a_message_when_team_is_deleted.yml
@@ -0,0 +1,37 @@
+langcode: en
+status: true
+dependencies: { }
+id: log_a_message_when_team_is_deleted
+label: 'Log a message when team is deleted.'
+events:
+ -
+ event_name: 'apigee_edge_actions_entity_delete:team'
+description: 'Logs a message when a team is deleted.'
+tags:
+ - example
+config_version: '3'
+expression:
+ id: rules_rule
+ uuid: 46a54411-060d-4b6d-bbef-ca7d3067ecc9
+ conditions:
+ id: rules_and
+ uuid: b8fa61f6-ddb0-4a97-9729-617298ac6d8f
+ conditions: { }
+ actions:
+ id: rules_action_set
+ uuid: 525c38c8-cf9f-4424-b084-443079d8553a
+ actions:
+ -
+ id: rules_action
+ uuid: 632d4a81-8383-4343-82af-cabefb0824e0
+ context_values:
+ message: 'The team {{ team.displayName }} was deleted.'
+ level: notice
+ context_mapping: { }
+ context_processors:
+ message:
+ rules_tokens: { }
+ level:
+ rules_tokens: { }
+ provides_mapping: { }
+ action_id: apigee_edge_actions_log_message
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_developer_when_added_to_a_team.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_developer_when_added_to_a_team.yml
new file mode 100644
index 00000000..3c0dbd1c
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_developer_when_added_to_a_team.yml
@@ -0,0 +1,47 @@
+langcode: en
+status: true
+dependencies: { }
+id: notify_developer_when_added_to_a_team
+label: 'Notify developer when added to a team'
+events:
+ -
+ event_name: 'apigee_edge_actions_entity_add_member:team'
+description: 'Sends an email to a developer when added to a team.'
+tags:
+ - example
+config_version: '3'
+expression:
+ id: rules_rule
+ uuid: 0b36c5a1-b1f4-43ba-b3c1-b70859d22522
+ conditions:
+ id: rules_and
+ uuid: 9f479df0-8312-49a4-bda3-e27091f13acb
+ conditions: { }
+ actions:
+ id: rules_action_set
+ uuid: fd94da7a-428d-446d-afa1-5e63725d3258
+ actions:
+ -
+ id: rules_action
+ uuid: 5415ce2c-3238-4458-a472-2ac8097c07a1
+ context_values:
+ to:
+ - '{{ member.mail }}'
+ subject: 'You have been added to the {{ team.displayName }} team.'
+ message: 'Hey {{ member.first_name }}, you have been added to the {{ team.displayName }} team.'
+ reply: ''
+ language: ''
+ context_mapping: { }
+ context_processors:
+ to:
+ rules_tokens: { }
+ subject:
+ rules_tokens: { }
+ message:
+ rules_tokens: { }
+ reply:
+ rules_tokens: { }
+ language:
+ rules_tokens: { }
+ provides_mapping: { }
+ action_id: rules_send_email
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_developer_when_adding_a_new_app.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_developer_when_adding_a_new_app.yml
new file mode 100644
index 00000000..032efcde
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_developer_when_adding_a_new_app.yml
@@ -0,0 +1,47 @@
+langcode: en
+status: true
+dependencies: { }
+id: notify_developer_when_adding_a_new_app
+label: 'Notify developer when adding a new app'
+events:
+ -
+ event_name: 'apigee_edge_actions_entity_insert:developer_app'
+description: 'Sends an email to the developer when adding a new app.'
+tags:
+ - example
+config_version: '3'
+expression:
+ id: rules_rule
+ uuid: 9863eac4-9005-4185-a078-e979eb1e0668
+ conditions:
+ id: rules_and
+ uuid: 0874eaac-3379-4b4c-8653-74ce89ae9c1f
+ conditions: { }
+ actions:
+ id: rules_action_set
+ uuid: 5f2f78a9-c970-4d7c-9ada-30dca4133d2d
+ actions:
+ -
+ id: rules_action
+ uuid: e4ec54f4-917a-413b-a410-8089296058f6
+ context_values:
+ to:
+ - '{{ developer.mail }}'
+ subject: 'App {{ developer_app.displayName }} was created.'
+ message: 'App {{ developer_app.displayName }} was successfully created.'
+ reply: ''
+ language: ''
+ context_mapping: { }
+ context_processors:
+ to:
+ rules_tokens: { }
+ subject:
+ rules_tokens: { }
+ message:
+ rules_tokens: { }
+ reply:
+ rules_tokens: { }
+ language:
+ rules_tokens: { }
+ provides_mapping: { }
+ action_id: rules_send_email
diff --git a/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_site_admins_when_app_is_created.yml b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_site_admins_when_app_is_created.yml
new file mode 100644
index 00000000..27c69c99
--- /dev/null
+++ b/modules/apigee_edge_actions/modules/apigee_edge_actions_examples/config/optional/rules.reaction.notify_site_admins_when_app_is_created.yml
@@ -0,0 +1,47 @@
+langcode: en
+status: true
+dependencies: { }
+id: notify_site_admins_when_app_is_created
+label: 'Notify site admins when app is created'
+events:
+ -
+ event_name: 'apigee_edge_actions_entity_insert:developer_app'
+description: 'Send an email to site administrators when an app is created.'
+tags:
+ - example
+config_version: '3'
+expression:
+ id: rules_rule
+ uuid: a3c7f453-75a4-4d1b-87f7-973831d18365
+ conditions:
+ id: rules_and
+ uuid: 3e95f490-4379-412e-9bf3-2a2b8b68e6e3
+ conditions: { }
+ actions:
+ id: rules_action_set
+ uuid: ea47410d-065e-4cf4-8f8e-9fafdcdbee9b
+ actions:
+ -
+ id: rules_action
+ uuid: b2826e2c-5322-4dfe-9020-fc622f0bbf2e
+ context_values:
+ roles:
+ - administrator
+ subject: 'App {{ developer_app.displayName }} was added to the site.'
+ message: 'App {{ developer_app.displayName }} was added to the site.'
+ reply: ''
+ language: ''
+ context_mapping: { }
+ context_processors:
+ roles:
+ rules_tokens: { }
+ subject:
+ rules_tokens: { }
+ message:
+ rules_tokens: { }
+ reply:
+ rules_tokens: { }
+ language:
+ rules_tokens: { }
+ provides_mapping: { }
+ action_id: rules_email_to_users_of_role
diff --git a/modules/apigee_edge_actions/src/ApigeeActionsEntityTypeHelper.php b/modules/apigee_edge_actions/src/ApigeeActionsEntityTypeHelper.php
new file mode 100644
index 00000000..a246ac1d
--- /dev/null
+++ b/modules/apigee_edge_actions/src/ApigeeActionsEntityTypeHelper.php
@@ -0,0 +1,65 @@
+entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypes(): array {
+ return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
+ return $this->isFieldableEdgeEntityType($entity_type);
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFieldableEdgeEntityType(EntityTypeInterface $entity_type): bool {
+ return $entity_type->entityClassImplements(FieldableEdgeEntityInterface::class);
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/ApigeeActionsEntityTypeHelperInterface.php b/modules/apigee_edge_actions/src/ApigeeActionsEntityTypeHelperInterface.php
new file mode 100644
index 00000000..c3e36be0
--- /dev/null
+++ b/modules/apigee_edge_actions/src/ApigeeActionsEntityTypeHelperInterface.php
@@ -0,0 +1,49 @@
+has('apigee_edge_teams.team_membership_manager')) {
+ $container->register('apigee_edge_actions.team_membership_manager', TeamMembershipManager::class)
+ ->setDecoratedService('apigee_edge_teams.team_membership_manager')
+ ->setArguments([
+ new Reference('apigee_edge_actions.team_membership_manager.inner'),
+ new Reference('entity_type.manager'),
+ new Reference('apigee_edge_teams.company_members_controller_factory'),
+ new Reference('apigee_edge.controller.developer'),
+ new Reference('apigee_edge.controller.cache.developer_companies'),
+ new Reference('cache_tags.invalidator'),
+ new Reference('logger.channel.apigee_edge_teams'),
+ new Reference('event_dispatcher'),
+ ]);
+ }
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Event/ApigeeEdgeActionsEventInterface.php b/modules/apigee_edge_actions/src/Event/ApigeeEdgeActionsEventInterface.php
new file mode 100644
index 00000000..a06c7b83
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Event/ApigeeEdgeActionsEventInterface.php
@@ -0,0 +1,28 @@
+entityTypeManger = $entity_type_manager;
+ $this->logger = $logger;
+ $this->eventDispatcher = $event_dispatcher;
+ $this->currentUser = $current_user;
+ }
+
+ /**
+ * Responds to add product events.
+ *
+ * @param \Drupal\apigee_edge\Event\AppCredentialAddApiProductEvent $event
+ * The app credential add product event.
+ */
+ public function onAddProduct(AppCredentialAddApiProductEvent $event) {
+ $this->dispatchRulesEvent('apigee_edge_actions_entity_add_product:developer_app', $event, $event->getNewProducts());
+ }
+
+ /**
+ * Responds to remove product events.
+ *
+ * @param \Drupal\apigee_edge\Event\AppCredentialDeleteApiProductEvent $event
+ * The app credential remove product event.
+ */
+ public function onRemoveProduct(AppCredentialDeleteApiProductEvent $event) {
+ $this->dispatchRulesEvent('apigee_edge_actions_entity_remove_product:developer_app', $event, [$event->getApiProduct()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ return [
+ AppCredentialAddApiProductEvent::EVENT_NAME => ['onAddProduct', 100],
+ AppCredentialDeleteApiProductEvent::EVENT_NAME => ['onRemoveProduct', 100],
+ ];
+ }
+
+ /**
+ * Helper to dispatch a corresponding rules event for an api credential event.
+ *
+ * @param string $rules_event_name
+ * The name of the rules event.
+ * @param \Symfony\Component\EventDispatcher\Event $event
+ * The api credential event.
+ * @param array $api_products
+ * An array of api products.
+ */
+ protected function dispatchRulesEvent(string $rules_event_name, Event $event, array $api_products) {
+ try {
+ $app = $this->getAppByName($event->getAppName(), $event->getOwnerId(), $event->getAppType());
+ $app_type = "{$event->getAppType()}_app";
+
+ if ('developer_app' == $app_type) {
+ // For developer apps, get the Drupal account from the app owner.
+ /** @var \Drupal\apigee_edge\Entity\Storage\DeveloperStorageInterface $developer_storage */
+ /** @var \Drupal\apigee_edge\Entity\Developer $owner */
+ $developer_storage = $this->entityTypeManger->getStorage($event->getAppType());
+ $owner = $developer_storage->load($event->getOwnerId());
+ $developer = user_load_by_mail($owner->getEmail());
+ }
+ else {
+ // For team apps, default to the current user.
+ $developer = $this->entityTypeManger->getStorage('user')
+ ->load($this->currentUser->id());
+ }
+
+ foreach ($api_products as $product) {
+ /** @var \Drupal\apigee_edge\Entity\ApiProductInterface $api_product */
+ $api_product = $this->entityTypeManger
+ ->getStorage('api_product')
+ ->load($product);
+ $this->eventDispatcher->dispatch($rules_event_name, new EdgeEntityEventEdge($app, [
+ $app_type => $app,
+ 'developer' => $developer,
+ 'api_product_name' => $api_product->getName(),
+ 'api_product_display_name' => $api_product->getDisplayName(),
+ ]));
+ }
+ }
+ catch (PluginException $exception) {
+ $this->logger->error($exception->getMessage());
+ }
+ }
+
+ /**
+ * Helper to load an app by name.
+ *
+ * @param string $name
+ * The name of the app.
+ * @param string $owner_id
+ * The developer or team.
+ * @param string $app_type
+ * The type of the app.
+ *
+ * @return \Drupal\apigee_edge\Entity\AppInterface|null
+ * The app with the provided name or null.
+ */
+ protected function getAppByName(string $name, string $owner_id, string $app_type): ?AppInterface {
+ /* @var \Drupal\apigee_edge\Entity\AppInterface $appClass */
+ $appClass = $this->entityTypeManger->getStorage("{$app_type}_app")->getEntityType()->getClass();
+
+ try {
+ if ($app_type == 'developer') {
+ /* @var \Drupal\apigee_edge\Entity\Controller\DeveloperAppControllerFactoryInterface $controller */
+ $controller = \Drupal::service('apigee_edge.controller.developer_app_controller_factory');
+ $edge_app = $controller->developerAppController($owner_id)->load($name);
+ }
+ else {
+ /* @var \Drupal\apigee_edge_teams\Entity\Controller\TeamAppControllerFactory $controller */
+ $controller = \Drupal::service('apigee_edge_teams.controller.team_app_controller_factory');
+ $edge_app = $controller->teamAppController($owner_id)->load($name);
+ }
+
+ $app = $appClass::createFrom($edge_app);
+
+ return $app;
+ }
+ catch (PluginException $exception) {
+ $this->logger->error($exception);
+ }
+
+ return NULL;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesAction/LogMessage.php b/modules/apigee_edge_actions/src/Plugin/RulesAction/LogMessage.php
new file mode 100644
index 00000000..4b3f6ec0
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesAction/LogMessage.php
@@ -0,0 +1,101 @@
+logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('logger.channel.apigee_edge_actions')
+ );
+ }
+
+ /**
+ * Executes the action.
+ *
+ * @param string $message
+ * The message.
+ * @param string $level
+ * The log level.
+ */
+ protected function doExecute(string $message, string $level = "notice") {
+ if (method_exists($this->logger, $level)) {
+ $this->logger->{$level}($message);
+ }
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesAction/SystemMailToUsersOfRole.php b/modules/apigee_edge_actions/src/Plugin/RulesAction/SystemMailToUsersOfRole.php
new file mode 100644
index 00000000..7cae4509
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesAction/SystemMailToUsersOfRole.php
@@ -0,0 +1,92 @@
+roleStorage = $role_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('logger.factory')->get('rules'),
+ $container->get('plugin.manager.mail'),
+ $container->get('entity_type.manager')->getStorage('user'),
+ $container->get('entity_type.manager')->getStorage('user_role')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doExecute(array $roles, $subject, $message, $reply = NULL, LanguageInterface $language = NULL) {
+ // SystemMailToUsersOfRole::doExecute() expects an array of RoleInterface.
+ // Upcast $roles from string[] to RoleInterface[].
+ // @see https://www.drupal.org/project/rules/issues/2800749
+ $roles = $this->roleStorage->loadMultiple($roles);
+ parent::doExecute($roles, $subject, $message, $reply, $language);
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityAddMemberEventDeriver.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityAddMemberEventDeriver.php
new file mode 100644
index 00000000..c557f747
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityAddMemberEventDeriver.php
@@ -0,0 +1,63 @@
+t('After adding a team member');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypes(): array {
+ // Filter out non team entity types.
+ return array_filter(parent::getEntityTypes(), function (EdgeEntityTypeInterface $entity_type) {
+ return $entity_type->entityClassImplements(TeamInterface::class);
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext(EdgeEntityTypeInterface $entity_type): array {
+ $context = parent::getContext($entity_type);
+
+ // Add the team member to the context.
+ $context['member'] = [
+ 'type' => 'entity:user',
+ 'label' => $this->t('Member'),
+ ];
+
+ return $context;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityAddProductEventDeriver.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityAddProductEventDeriver.php
new file mode 100644
index 00000000..3f558c07
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityAddProductEventDeriver.php
@@ -0,0 +1,37 @@
+t('After adding an API product');
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityDeleteEventDeriver.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityDeleteEventDeriver.php
new file mode 100644
index 00000000..52c9fbfd
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityDeleteEventDeriver.php
@@ -0,0 +1,37 @@
+t('After deleting a @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]);
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityEventDeriverBase.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityEventDeriverBase.php
new file mode 100644
index 00000000..fedf477f
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityEventDeriverBase.php
@@ -0,0 +1,118 @@
+edgeEntityTypeManager = $edge_entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('apigee_edge_actions.edge_entity_type_manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypes(): array {
+ return $this->edgeEntityTypeManager->getEntityTypes();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext(EdgeEntityTypeInterface $entity_type): array {
+ $context = [
+ $entity_type->id() => [
+ 'type' => "entity:{$entity_type->id()}",
+ 'label' => $entity_type->getLabel(),
+ ],
+ ];
+
+ // Add additional context for App.
+ if ($entity_type->entityClassImplements(AppInterface::class)) {
+ // Add the developer to the context.
+ $context['developer'] = [
+ 'type' => 'entity:user',
+ 'label' => 'Developer',
+ ];
+
+ // Add the team to the context.
+ if ($entity_type->entityClassImplements(TeamAppInterface::class)) {
+ $context['team'] = [
+ 'type' => 'entity:team',
+ 'label' => 'Team',
+ ];
+ }
+ }
+
+ return $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ foreach ($this->getEntityTypes() as $entity_type) {
+ $this->derivatives[$entity_type->id()] = [
+ 'label' => $this->getLabel($entity_type),
+ 'category' => $entity_type->getLabel(),
+ 'entity_type_id' => $entity_type->id(),
+ 'context' => $this->getContext($entity_type),
+ ] + $base_plugin_definition;
+ }
+
+ return $this->derivatives;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityEventDeriverInterface.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityEventDeriverInterface.php
new file mode 100644
index 00000000..1ea24d90
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityEventDeriverInterface.php
@@ -0,0 +1,61 @@
+t('After saving a new @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]);
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityProductEventDeriverBase.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityProductEventDeriverBase.php
new file mode 100644
index 00000000..5f0aeae1
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityProductEventDeriverBase.php
@@ -0,0 +1,63 @@
+entityClassImplements(AppInterface::class);
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext(EdgeEntityTypeInterface $entity_type): array {
+ $context = parent::getContext($entity_type);
+
+ // The api_product entity type is not fieldable hence does not support typed
+ // data. We have to add the attributes individually here.
+ $context['api_product_name'] = [
+ 'type' => 'string',
+ 'label' => $this->t('Name'),
+ ];
+ $context['api_product_display_name'] = [
+ 'type' => 'string',
+ 'label' => $this->t('Display name'),
+ ];
+
+ return $context;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityRemoveMemberEventDeriver.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityRemoveMemberEventDeriver.php
new file mode 100644
index 00000000..cda50a1c
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityRemoveMemberEventDeriver.php
@@ -0,0 +1,63 @@
+t('After removing a team member');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypes(): array {
+ // Filter out non team entity types.
+ return array_filter(parent::getEntityTypes(), function (EdgeEntityTypeInterface $entity_type) {
+ return $entity_type->entityClassImplements(TeamInterface::class);
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext(EdgeEntityTypeInterface $entity_type): array {
+ $context = parent::getContext($entity_type);
+
+ // Add the team member to the context.
+ $context['member'] = [
+ 'type' => 'entity:user',
+ 'label' => $this->t('Member'),
+ ];
+
+ return $context;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityRemoveProductEventDeriver.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityRemoveProductEventDeriver.php
new file mode 100644
index 00000000..3309f975
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityRemoveProductEventDeriver.php
@@ -0,0 +1,37 @@
+t('After removing an API product');
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityUpdateEventDeriver.php b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityUpdateEventDeriver.php
new file mode 100644
index 00000000..5a7a0e65
--- /dev/null
+++ b/modules/apigee_edge_actions/src/Plugin/RulesEvent/EdgeEntityUpdateEventDeriver.php
@@ -0,0 +1,52 @@
+t('After updating a @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext(EdgeEntityTypeInterface $entity_type): array {
+ $context = parent::getContext($entity_type);
+
+ // Add the original entity to the context.
+ $context["{$entity_type->id()}_unchanged"] = [
+ 'type' => "entity:{$entity_type->id()}",
+ 'label' => $this->t('Unchanged @entity_type', ['@entity_type' => $entity_type->getLabel()]),
+ ];
+
+ return $context;
+ }
+
+}
diff --git a/modules/apigee_edge_actions/src/TeamMembershipManager.php b/modules/apigee_edge_actions/src/TeamMembershipManager.php
new file mode 100644
index 00000000..03a7ebcf
--- /dev/null
+++ b/modules/apigee_edge_actions/src/TeamMembershipManager.php
@@ -0,0 +1,187 @@
+inner = $inner;
+ $this->entityTypeManager = $entity_type_manager;
+ $this->companyMembersControllerFactory = $company_members_controller_factory;
+ $this->developerController = $developer_controller;
+ $this->developerCompaniesCache = $developer_companies_cache;
+ $this->cacheTagsInvalidator = $cache_tags_invalidator;
+ $this->logger = $logger;
+ $this->eventDispatcher = $event_dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMembers(string $team): array {
+ return $this->inner->getMembers($team);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addMembers(string $team, array $developers): void {
+ $this->inner->addMembers($team, $developers);
+
+ $this->dispatchEvent('apigee_edge_actions_entity_add_member:team', $team, $developers);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeMembers(string $team, array $developers): void {
+ $this->inner->removeMembers($team, $developers);
+
+ $this->dispatchEvent('apigee_edge_actions_entity_remove_member:team', $team, $developers);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTeams(string $developer): array {
+ return $this->inner->getTeams($developer);
+ }
+
+ /**
+ * Helper to dispatch event.
+ *
+ * @param string $event
+ * The event name.
+ * @param string $team
+ * The team id.
+ * @param array $developers
+ * An array of developers.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ protected function dispatchEvent(string $event, string $team, array $developers) {
+ $team = $this->entityTypeManager->getStorage('team')->load($team);
+ $users_by_mail = array_reduce($this->entityTypeManager->getStorage('user')->loadByProperties(['mail' => $developers]), function (array $carry, UserInterface $user) {
+ $carry[$user->getEmail()] = $user;
+ return $carry;
+ }, []);
+
+ // Dispatch an event for each developer.
+ foreach ($developers as $developer) {
+ $this->eventDispatcher->dispatch($event, new EdgeEntityEventEdge($team, [
+ 'team' => $team,
+ 'member' => $users_by_mail[$developer],
+ ]));
+ }
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/ApigeeEdgeActionsRulesKernelTestBase.php b/modules/apigee_edge_actions/tests/src/Kernel/ApigeeEdgeActionsRulesKernelTestBase.php
new file mode 100644
index 00000000..5f8744c1
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/ApigeeEdgeActionsRulesKernelTestBase.php
@@ -0,0 +1,116 @@
+storage = $this->container->get('entity_type.manager')->getStorage('rules_reaction_rule');
+
+ $this->installConfig(['apigee_edge']);
+ $this->installEntitySchema('user');
+ $this->installSchema('dblog', ['watchdog']);
+ $this->installSchema('system', ['sequences']);
+ $this->installSchema('user', ['users_data']);
+
+ $this->baseSetUp();
+
+ /** @var \Drupal\user\UserInterface $account */
+ $this->account = User::create([
+ 'mail' => $this->randomMachineName() . '@example.com',
+ 'name' => $this->randomMachineName(),
+ 'first_name' => $this->getRandomGenerator()->word(16),
+ 'last_name' => $this->getRandomGenerator()->word(16),
+ ]);
+ $this->account->save();
+ $this->queueDeveloperResponse($this->account, Response::HTTP_CREATED);
+ }
+
+ /**
+ * Helper to assert logs.
+ *
+ * @param string $message
+ * The message to assert in the logs.
+ * @param string $type
+ * The type for the log.
+ */
+ protected function assertLogsContains(string $message, $type = 'apigee_edge_actions') {
+ $logs = Database::getConnection()->select('watchdog', 'wd')
+ ->fields('wd', ['message', 'variables'])
+ ->condition('type', $type)
+ ->execute()
+ ->fetchAll();
+
+ $controller = DbLogController::create($this->container);
+ $messages = array_map(function ($log) use ($controller) {
+ return (string) $controller->formatMessage($log);
+ }, $logs);
+
+ $this->assertContains($message, $messages);
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesAction/SystemMailToUsersOfRoleTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesAction/SystemMailToUsersOfRoleTest.php
new file mode 100644
index 00000000..fd359d3d
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesAction/SystemMailToUsersOfRoleTest.php
@@ -0,0 +1,75 @@
+container->get('entity_type.manager')->getStorage('user_role');
+ $role_storage->create(['id' => 'test_role'])->save();
+ $this->account->addRole('test_role');
+ $this->queueDeveloperResponse($this->account);
+ $this->account->activate();
+ $this->account->save();
+
+ $rule = $this->expressionManager->createRule();
+ $rule->addAction('rules_email_to_users_of_role',
+ ContextConfig::create()
+ ->setValue('roles', ['test_role'])
+ ->setValue('subject', 'Test email')
+ ->setValue('message', 'This is a test email')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'send_email_to_admin_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_insert:developer_app']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ // Insert an entity to trigger rule.
+ $this->queueDeveloperResponse($this->account);
+ $this->createDeveloperApp();
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_insert:developer_app was dispatched.");
+ $this->assertLogsContains('Successfully sent email to 1 out of 1 users having the role(s) test_role', 'rules');
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityAddMemberEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityAddMemberEventTest.php
new file mode 100644
index 00000000..d61f1ff1
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityAddMemberEventTest.php
@@ -0,0 +1,95 @@
+installEntitySchema('team_member_role');
+ }
+
+ /**
+ * Tests add_member events for Edge entities.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ * @throws \Drupal\rules\Exception\LogicException
+ */
+ public function testEvent() {
+ // Create an add_member rule.
+ $rule = $this->expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "Member {{ member.first_name }} was added to team {{ team.displayName }}.")
+ ->process('message', 'rules_tokens')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_add_member_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_add_member:team']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ // Create a new team.
+ $team = $this->createTeam();
+
+ // Add team member.
+ $this->queueCompanyResponse($team->decorated());
+ $this->queueDeveloperResponse($this->account);
+ $this->container->get('apigee_edge_teams.team_membership_manager')->addMembers($team->id(), [
+ $this->account->getEmail(),
+ ]);
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_add_member:team was dispatched.");
+ $this->assertLogsContains("Member {$this->account->first_name->value} was added to team {$team->getDisplayName()}.");
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityAddProductEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityAddProductEventTest.php
new file mode 100644
index 00000000..8b4d9288
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityAddProductEventTest.php
@@ -0,0 +1,118 @@
+randomMachineName();
+ // Create an insert rule.
+ $rule = $this->expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "Product {{ api_product_name }} was added to app {{ developer_app.name }}.")
+ ->process('message', 'rules_tokens')
+ );
+
+ // Test condition.
+ $rule->addCondition('rules_data_comparison', ContextConfig::create()
+ ->map('data', 'api_product_name')
+ ->setValue('value', $api_product_name)
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_insert_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_add_product:developer_app']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ /** @var \Drupal\apigee_edge\Entity\AppInterface $developer_app */
+ $developer_app = $this->createDeveloperApp();
+
+ $api_product = ApiProduct::create([
+ 'name' => $api_product_name,
+ 'displayName' => $this->getRandomGenerator()->word(16),
+ 'approvalType' => ApiProduct::APPROVAL_TYPE_AUTO,
+ ]);
+
+ /** @var \Drupal\apigee_edge\Entity\ApiProduct $api_product */
+ $this->stack->queueMockResponse([
+ 'api_product' => [
+ 'product' => $api_product,
+ ],
+ ]);
+
+ $api_product->save();
+
+ $this->stack->queueMockResponse([
+ 'api_product' => [
+ 'product' => $api_product,
+ ],
+ ]);
+ $this->queueDeveloperResponse($this->account);
+ $this->stack->queueMockResponse([
+ 'get_developer_apps' => [
+ 'apps' => [$developer_app]
+ ],
+ ]);
+
+ /** @var \Drupal\apigee_edge\Entity\Controller\DeveloperAppCredentialControllerFactoryInterface $credential_factory */
+ $credential_factory = \Drupal::service('apigee_edge.controller.developer_app_credential_factory');
+ $credential_factory->developerAppCredentialController($this->account->uuid(), $developer_app->getName())->addProducts($this->randomString(), [$api_product->id()]);
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_add_product:developer_app was dispatched.");
+ $this->assertLogsContains("Product {$api_product->getName()} was added to app {$developer_app->getName()}.");
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityDeleteEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityDeleteEventTest.php
new file mode 100644
index 00000000..7c86bfd2
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityDeleteEventTest.php
@@ -0,0 +1,67 @@
+expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "App {{ developer_app.name }} was deleted.")
+ ->process('message', 'rules_tokens')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_delete_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_delete:developer_app']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ // Insert and delete entity.
+ $entity = $this->createDeveloperApp();
+ $this->queueDeveloperAppResponse($entity);
+ $entity->delete();
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_delete:developer_app was dispatched.");
+ $this->assertLogsContains("App {$entity->getName()} was deleted.");
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityInsertEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityInsertEventTest.php
new file mode 100644
index 00000000..3942b2fd
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityInsertEventTest.php
@@ -0,0 +1,65 @@
+expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "App {{ developer_app.name }} was created by {{ developer.first_name }}.")
+ ->process('message', 'rules_tokens')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_insert_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_insert:developer_app']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ // Insert an entity.
+ $entity = $this->createDeveloperApp();
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_insert:developer_app was dispatched.");
+ $this->assertLogsContains("App {$entity->getName()} was created by {$this->account->first_name->value}.");
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityRemoveMemberEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityRemoveMemberEventTest.php
new file mode 100644
index 00000000..62387ec7
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityRemoveMemberEventTest.php
@@ -0,0 +1,100 @@
+installEntitySchema('team_member_role');
+ }
+
+ /**
+ * Tests add_member events for Edge entities.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ * @throws \Drupal\rules\Exception\LogicException
+ */
+ public function testEvent() {
+ // Create an remove_member rule.
+ $rule = $this->expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "Member {{ member.first_name }} was removed from team {{ team.displayName }}.")
+ ->process('message', 'rules_tokens')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_remove_member_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_remove_member:team']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ // Create a new team.
+ $team = $this->createTeam();
+
+ // Add team member.
+ $this->queueCompanyResponse($team->decorated());
+ $this->queueDeveloperResponse($this->account);
+ $team_membership_manager = $this->container->get('apigee_edge_teams.team_membership_manager');
+ $team_membership_manager->addMembers($team->id(), [
+ $this->account->getEmail(),
+ ]);
+
+ // Remove team member.
+ $team_membership_manager->removeMembers($team->id(), [
+ $this->account->getEmail(),
+ ]);
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_remove_member:team was dispatched.");
+ $this->assertLogsContains("Member {$this->account->first_name->value} was removed from team {$team->getDisplayName()}.");
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityRemoveProductEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityRemoveProductEventTest.php
new file mode 100644
index 00000000..10b06000
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityRemoveProductEventTest.php
@@ -0,0 +1,122 @@
+expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "Product {{ api_product_name }} was removed from app {{ developer_app.name }}.")
+ ->process('message', 'rules_tokens')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_insert_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_remove_product:developer_app']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ /** @var \Drupal\apigee_edge\Entity\AppInterface $developer_app */
+ $developer_app = $this->createDeveloperApp();
+
+ $api_product = ApiProduct::create([
+ 'name' => $this->randomMachineName(),
+ 'displayName' => $this->getRandomGenerator()->word(16),
+ 'approvalType' => ApiProduct::APPROVAL_TYPE_AUTO,
+ ]);
+
+ /** @var \Drupal\apigee_edge\Entity\ApiProduct $api_product */
+ $this->stack->queueMockResponse([
+ 'api_product' => [
+ 'product' => $api_product,
+ ],
+ ]);
+
+ $api_product->save();
+
+ $this->stack->queueMockResponse([
+ 'api_product' => [
+ 'product' => $api_product,
+ ],
+ ]);
+ $this->queueDeveloperResponse($this->account);
+ $this->queueDeveloperAppResponse($developer_app);
+
+ /** @var \Drupal\apigee_edge\Entity\Controller\DeveloperAppCredentialControllerFactoryInterface $credential_factory */
+ $credential_factory = \Drupal::service('apigee_edge.controller.developer_app_credential_factory');
+ /** @var \Drupal\apigee_edge\Entity\Controller\AppCredentialControllerInterface $app_credential_controller */
+ $app_credential_controller = $credential_factory->developerAppCredentialController($this->account->uuid(), $developer_app->getName());
+ $consumer_key = $this->randomString();
+ $app_credential_controller->addProducts($consumer_key, [$api_product->id()]);
+
+ $this->stack->queueMockResponse([
+ 'api_product' => [
+ 'product' => $api_product,
+ ],
+ ]);
+ $this->stack->queueMockResponse([
+ 'get_developer_apps' => [
+ 'apps' => [$developer_app]
+ ],
+ ]);
+ $app_credential_controller->deleteApiProduct($consumer_key, $api_product->id());
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_remove_product:developer_app was dispatched.");
+ $this->assertLogsContains("Product {$api_product->getName()} was removed from app {$developer_app->getName()}.");
+ }
+
+}
diff --git a/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityUpdateEventTest.php b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityUpdateEventTest.php
new file mode 100644
index 00000000..7503c678
--- /dev/null
+++ b/modules/apigee_edge_actions/tests/src/Kernel/Plugin/RulesEvent/EdgeEntityUpdateEventTest.php
@@ -0,0 +1,72 @@
+expressionManager->createRule();
+ $rule->addAction('apigee_edge_actions_log_message',
+ ContextConfig::create()
+ ->setValue('message', "App {{ developer_app_unchanged.displayName }} was renamed to {{ developer_app.displayName }}.")
+ ->process('message', 'rules_tokens')
+ );
+
+ $config_entity = $this->storage->create([
+ 'id' => 'app_update_rule',
+ 'events' => [['event_name' => 'apigee_edge_actions_entity_update:developer_app']],
+ 'expression' => $rule->getConfiguration(),
+ ]);
+ $config_entity->save();
+
+ // Insert and update entity.
+ /** @var \Drupal\apigee_edge\Entity\DeveloperAppInterface $entity */
+ $entity = $this->createDeveloperApp();
+ $original_name = $entity->getDisplayName();
+ $new_name = $this->randomGenerator->name();
+ $this->queueDeveloperAppResponse($entity);
+ $entity->setDisplayName($new_name);
+ $this->queueDeveloperAppResponse($entity);
+ $entity->save();
+
+ $this->assertLogsContains("Event apigee_edge_actions_entity_update:developer_app was dispatched.");
+ $this->assertLogsContains("App $original_name was renamed to $new_name.");
+ }
+
+}
diff --git a/tests/modules/apigee_mock_api_client/tests/response-templates/get-developer-apps.json.twig b/tests/modules/apigee_mock_api_client/tests/response-templates/get-developer-apps.json.twig
new file mode 100644
index 00000000..dbb2bd4b
--- /dev/null
+++ b/tests/modules/apigee_mock_api_client/tests/response-templates/get-developer-apps.json.twig
@@ -0,0 +1,19 @@
+{#
+/**
+ * @file
+ * GET /v1/organizations/{org_name}/developers/{developer_email_or_id}/apps
+ *
+ * Response Code: 200
+ *
+ * Variables:
+ * - org_name: The name of the org.
+ * - developer_email_or_id: The developer email or id.
+ */
+#}
+{
+ "apps" : [
+ {% for app in apps %}
+ {% include 'developer-app.json.twig' with {'app': app} %}{{ loop.last ? '' : ',' }}
+ {% endfor %}
+ ]
+}
diff --git a/tests/modules/apigee_mock_api_client/tests/src/Traits/ApigeeMockApiClientHelperTrait.php b/tests/modules/apigee_mock_api_client/tests/src/Traits/ApigeeMockApiClientHelperTrait.php
index 8faada70..fac9f1f3 100644
--- a/tests/modules/apigee_mock_api_client/tests/src/Traits/ApigeeMockApiClientHelperTrait.php
+++ b/tests/modules/apigee_mock_api_client/tests/src/Traits/ApigeeMockApiClientHelperTrait.php
@@ -19,12 +19,16 @@
namespace Drupal\Tests\apigee_mock_api_client\Traits;
+use Apigee\Edge\Api\Management\Entity\App;
use Apigee\Edge\Api\Management\Entity\Company;
use Apigee\Edge\Api\Management\Entity\Organization;
use Apigee\MockClient\Generator\ApigeeSdkEntitySource;
use Drupal\apigee_edge\Entity\Developer;
+use Drupal\apigee_edge\Entity\DeveloperApp;
use Drupal\apigee_edge\Entity\DeveloperAppInterface;
use Drupal\apigee_edge\Entity\DeveloperInterface;
+use Drupal\apigee_edge_teams\Entity\Team;
+use Drupal\apigee_edge_teams\Entity\TeamInterface;
use Drupal\Tests\apigee_edge\Traits\ApigeeEdgeUtilTestTrait;
use Drupal\user\UserInterface;
use Http\Message\RequestMatcher\RequestMatcher;
@@ -223,7 +227,51 @@ protected function queueDevsInCompanyResponse(array $developers, $response_code
}
/**
- * Add an app analytics mock response to the stack.
+ * Helper to create a DeveloperApp entity.
+ *
+ * @return \Drupal\apigee_edge\Entity\DeveloperAppInterface
+ * A DeveloperApp entity.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ */
+ protected function createDeveloperApp(): DeveloperAppInterface {
+ /** @var \Drupal\apigee_edge\Entity\DeveloperAppInterface $entity */
+ $entity = DeveloperApp::create([
+ 'appId' => 1,
+ 'name' => $this->randomMachineName(),
+ 'status' => App::STATUS_APPROVED,
+ 'displayName' => $this->randomMachineName(),
+ ]);
+ $entity->setOwner($this->account);
+ $this->queueDeveloperAppResponse($entity);
+ $entity->save();
+
+ return $entity;
+ }
+
+ /**
+ * Helper to create a Team entity.
+ *
+ * @return \Drupal\apigee_edge_teams\Entity\TeamInterface
+ * A Team entity.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ */
+ protected function createTeam(): TeamInterface {
+ /** @var \Drupal\apigee_edge_teams\Entity\TeamInterface $team */
+ $team = Team::create([
+ 'name' => $this->randomMachineName(),
+ 'displayName' => $this->randomGenerator->name(),
+ ]);
+ $this->queueCompanyResponse($team->decorated());
+ $this->queueDeveloperResponse($this->account);
+ $team->save();
+
+ return $team;
+ }
+
+ /**
+ * Helper to add Edge entity response to stack.
*
* @param \Drupal\apigee_edge\Entity\DeveloperAppInterface $app
* The app.