From a743fff4179277a077f962501e7a27c867546306 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Mon, 7 Sep 2015 21:21:39 +0100 Subject: [PATCH] Added support for Run As User and Run Report Triggers from Process Builder --- .../EmailServicesAddressesSelector.cls | 58 ++++++++++++++ ...mailServicesAddressesSelector.cls-meta.xml | 4 + src/classes/LittleBitsActionReportTrigger.cls | 60 +++++++++++++++ ...LittleBitsActionReportTrigger.cls-meta.xml | 4 + .../LittleBitsDeviceSubscriptionsSelector.cls | 3 +- .../LittleBitsReportTriggersSelector.cls | 8 +- src/classes/LittleBitsService.cls | 75 ++++++++++++++++--- src/classes/LittleBitsServiceTest.cls | 38 ++++++++++ ...ittleBitsSubscriberInboundEmailHandler.cls | 33 ++++++++ ...SubscriberInboundEmailHandler.cls-meta.xml | 4 + ...ttleBits Device Subscription Layout.layout | 7 +- ..._c-LittleBits Report Trigger Layout.layout | 4 + .../LittleBitsDeviceSubscription__c.object | 12 +++ src/objects/LittleBitsReportTrigger__c.object | 11 +++ .../LittleBitsConnect.permissionset | 5 ++ 15 files changed, 311 insertions(+), 15 deletions(-) create mode 100644 src/classes/EmailServicesAddressesSelector.cls create mode 100644 src/classes/EmailServicesAddressesSelector.cls-meta.xml create mode 100644 src/classes/LittleBitsActionReportTrigger.cls create mode 100644 src/classes/LittleBitsActionReportTrigger.cls-meta.xml create mode 100644 src/classes/LittleBitsSubscriberInboundEmailHandler.cls create mode 100644 src/classes/LittleBitsSubscriberInboundEmailHandler.cls-meta.xml diff --git a/src/classes/EmailServicesAddressesSelector.cls b/src/classes/EmailServicesAddressesSelector.cls new file mode 100644 index 0000000..5af4d88 --- /dev/null +++ b/src/classes/EmailServicesAddressesSelector.cls @@ -0,0 +1,58 @@ +/** + * Copyright (c), Cory Cowgill and Andrew Fawcett + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Cory Cowgill and Andrew Fawcett, nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Class encapsulates query logic for EmailServicesAddress + * + * https://developer.salesforce.com/page/Apex_Enterprise_Patterns_-_Selector_Layer + **/ +public class EmailServicesAddressesSelector extends fflib_SObjectSelector +{ + public EmailServicesAddressesSelector() { + super(false, false, false); + } + + public List getSObjectFieldList() + { + return new List { + EmailServicesAddress.Id, + EmailServicesAddress.LocalPart, + EmailServicesAddress.EmailDomainName + }; + } + + public Schema.SObjectType getSObjectType() + { + return EmailServicesAddress.sObjectType; + } + + public List selectByUser(Set userIds) { + String functionName = 'LittleBitsSubscriberInboundEmailHandler'; + return Database.query( + newQueryFactory().setCondition( + 'Function.FunctionName = :functionName And IsActive = true and RunAsUserId in :userIds').toSOQL()); + } +} \ No newline at end of file diff --git a/src/classes/EmailServicesAddressesSelector.cls-meta.xml b/src/classes/EmailServicesAddressesSelector.cls-meta.xml new file mode 100644 index 0000000..7df4d0b --- /dev/null +++ b/src/classes/EmailServicesAddressesSelector.cls-meta.xml @@ -0,0 +1,4 @@ + + + 31.0 + \ No newline at end of file diff --git a/src/classes/LittleBitsActionReportTrigger.cls b/src/classes/LittleBitsActionReportTrigger.cls new file mode 100644 index 0000000..09616b6 --- /dev/null +++ b/src/classes/LittleBitsActionReportTrigger.cls @@ -0,0 +1,60 @@ +/** + * Copyright (c), Andrew Fawcett, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Andrew Fawcett, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +global with sharing class LittleBitsActionReportTrigger { + + global class Request { + @InvocableVariable(Label='Unique Name' Description='Unique Name of the Report Trigger' Required=true) + global String UniqueName; + } + + /** + * Run the given Report Trigger + **/ + @InvocableMethod(Label='Runs the given Report Trigger' Description='Runs the given Report Trigger.') + global static void run(List requests) { + Set uniqueNames = new Set(); + for(Request request : requests) + uniqueNames.add(request.UniqueName); + System.enqueueJob(new RunReportTriggerJob(uniqueNames)); + } + + /** + * Run the Report Triggers in Async since Invocable Methods from Process Builder don't support callouts + **/ + public class RunReportTriggerJob implements Queueable, Database.AllowsCallouts { + + private Set uniqueNames = new Set(); + + public RunReportTriggerJob(Set uniqueNames) { + this.uniqueNames = uniqueNames; + } + + public void execute(QueueableContext context) { + LittleBitsService.runReportTriggers(uniqueNames); + } + } +} \ No newline at end of file diff --git a/src/classes/LittleBitsActionReportTrigger.cls-meta.xml b/src/classes/LittleBitsActionReportTrigger.cls-meta.xml new file mode 100644 index 0000000..0f1c7ee --- /dev/null +++ b/src/classes/LittleBitsActionReportTrigger.cls-meta.xml @@ -0,0 +1,4 @@ + + + 34.0 + \ No newline at end of file diff --git a/src/classes/LittleBitsDeviceSubscriptionsSelector.cls b/src/classes/LittleBitsDeviceSubscriptionsSelector.cls index d7867cb..43cd0b4 100644 --- a/src/classes/LittleBitsDeviceSubscriptionsSelector.cls +++ b/src/classes/LittleBitsDeviceSubscriptionsSelector.cls @@ -39,7 +39,8 @@ public class LittleBitsDeviceSubscriptionsSelector extends fflib_SObjectSelector LittleBitsDeviceSubscription__c.DeviceID__c, LittleBitsDeviceSubscription__c.AccessToken__c, LittleBitsDeviceSubscription__c.Event__c, - LittleBitsDeviceSubscription__c.FlowName__c + LittleBitsDeviceSubscription__c.FlowName__c, + LittleBitsDeviceSubscription__c.RunAsUser__c }; } diff --git a/src/classes/LittleBitsReportTriggersSelector.cls b/src/classes/LittleBitsReportTriggersSelector.cls index dc7d899..3e057dc 100644 --- a/src/classes/LittleBitsReportTriggersSelector.cls +++ b/src/classes/LittleBitsReportTriggersSelector.cls @@ -40,7 +40,8 @@ public class LittleBitsReportTriggersSelector extends fflib_SObjectSelector LittleBitsReportTrigger__c.AggregateIndexForDuration__c, LittleBitsReportTrigger__c.AggregateIndexForPercent__c, LittleBitsReportTrigger__c.DeviceID__c, - LittleBitsReportTrigger__c.GroupingIndex__c + LittleBitsReportTrigger__c.GroupingIndex__c, + LittleBitsReportTrigger__c.UniqueName__c }; } @@ -54,6 +55,11 @@ public class LittleBitsReportTriggersSelector extends fflib_SObjectSelector return (List) selectSObjectsById(idSet); } + public List selectByUniqueName(Set uniqueNames) { + return Database.query( + newQueryFactory().setCondition('UniqueName__c in :uniqueNames').toSOQL()); + } + public Database.QueryLocator getAllActiveAsQueryLocator() { return Database.getQueryLocator( newQueryFactory().setCondition('Active__c = true').toSOQL()); diff --git a/src/classes/LittleBitsService.cls b/src/classes/LittleBitsService.cls index 91684f3..ed75b60 100644 --- a/src/classes/LittleBitsService.cls +++ b/src/classes/LittleBitsService.cls @@ -157,6 +157,17 @@ global class LittleBitsService { if(Trigger.isUpdate && Trigger.isAfter) triggerHandler(Trigger.oldMap, Trigger.newMap); } + + /** + * Processes the given LittleBits Report Trigger records, runs the reports outputs to the devices + * Based on the great work by Cory Cowgil: http://corycowgill.blogspot.co.uk/2014/12/create-real-life-dashboard-with.html + **/ + global static List runReportTriggers(Set uniqueNames) { + List reportTriggers = + new LittleBitsReportTriggersSelector().selectByUniqueName(uniqueNames); + Set reportTriggerIds = new Map(reportTriggers).keySet(); + return runReportTriggers(reportTriggerIds); + } /** * Processes the given LittleBits Report Trigger records, runs the reports outputs to the devices @@ -269,21 +280,41 @@ global class LittleBitsService { LittleBitsDeviceSubscription__c deviceSubscription = deviceSubscriptions[0]; flowParams.put('lbc_subscriptionRecordId', deviceSubscription.Id); - // Process the subscription and forward to a Flow - Type typeFactory = Type.forName('', FlowFactoryClass); - if(typeFactory==null) { - System.debug(FlowFactoryClass + ' class not found.'); - return; + // Run as User implement via Inbound Email Handler + if(deviceSubscription.RunAsUser__c!=null) { + // Determine the Inbound Email address configured for this user + List emailServices = + new EmailServicesAddressesSelector().selectByUser(new Set { deviceSubscription.RunAsUser__c }); + if(emailServices.size()==0) + throw new LittleBitsServiceException('No Inbound Email Service found for user.'); + if(emailServices.size()>1) + throw new LittleBitsServiceException('Multiple Inbound Email Services found for user.'); + // Send the email + String emailServiceAddress = emailServices[0].LocalPart + '@' + emailServices[0].EmailDomainName; + Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); + mail.setToAddresses(new String[] { emailServiceAddress }); + mail.setSubject(deviceSubscription.FlowName__c); + mail.setPlainTextBody(JSON.serialize(flowParams)); + Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail }); + } + else + { + // Invoke the flow as the running user + invokeFlow(deviceSubscription.FlowName__c, flowParams); } - ILittleBitsFlowFactory flowFactory = (ILittleBitsFlowFactory) typeFactory.newInstance(); - Flow.Interview flow = flowFactory.newInstance(deviceSubscription.FlowName__c, flowParams); - if(flow!=null) - flow.start(); - - // TODO: Think about some kind of logging mode or leave this purely to thd test mode i'm thinking about? - // .... } + /** + * Process an event from a LittleBits device sent via Inbound Email (these are not received in bulk) + * Envelope contents is assumed to be JSON serialised String, Object map based on the tools standard Flow params + **/ + global static void processSubscriptionEvent(Messaging.InboundEmail email) { + String flowName = email.subject; + Map flowParams = (Map) + JSON.deserializeUntyped(email.plainTextBody); + invokeFlow(flowName, flowParams); + } + @TestVisible private static String FlowFactoryClass = 'lbc_LittleBitsFlowFactory'; @@ -320,6 +351,26 @@ global class LittleBitsService { public class LittleBitsServiceException extends Exception {} + /** + * Dynamically invokes the given flow with the given params + **/ + private static void invokeFlow(String flowName, Map flowParams) { + + // Process the subscription and forward to a Flow + Type typeFactory = Type.forName('', FlowFactoryClass); + if(typeFactory==null) { + System.debug(FlowFactoryClass + ' class not found.'); + return; + } + ILittleBitsFlowFactory flowFactory = (ILittleBitsFlowFactory) typeFactory.newInstance(); + Flow.Interview flow = flowFactory.newInstance(flowName, flowParams); + if(flow!=null) + flow.start(); + + // TODO: Think about some kind of logging mode or leave this purely to thd test mode i'm thinking about? + // .... + } + /** * Obtain an instance of the LittleBits Apex Device class (leverages Custom Setting config routes accordingly) **/ diff --git a/src/classes/LittleBitsServiceTest.cls b/src/classes/LittleBitsServiceTest.cls index c3cbd53..29e63c5 100644 --- a/src/classes/LittleBitsServiceTest.cls +++ b/src/classes/LittleBitsServiceTest.cls @@ -133,6 +133,7 @@ private class LittleBitsServiceTest { reportTrigger.GroupingIndex__c = 0; reportTrigger.AggregateIndexForPercent__c = 1; reportTrigger.AggregateIndexForDuration__c = null; + reportTrigger.UniqueName__c = 'Test'; insert reportTrigger; Opportunity opp = new Opportunity(); opp.Name = 'Test'; @@ -150,6 +151,42 @@ private class LittleBitsServiceTest { System.assertEquals(true, mocksLittleBitsCloudAPI.calloutMade); } + @IsTest(SeeAllData=true) + // Why SeeAllData? https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_analytics_test_reports.htm + private static void callRunReportTriggersEnsureACalloutOccursByUniqueName() { + + // Create Mocks + MockLittleBitsOutputCloudAPI mocksLittleBitsCloudAPI = new MockLittleBitsOutputCloudAPI(); + Test.setMock(HttpCalloutMock.class, mocksLittleBitsCloudAPI); + + // Given + LittleBitsReportTrigger__c reportTrigger = new LittleBitsReportTrigger__c(); + reportTrigger.Name = 'Test'; + reportTrigger.ReportDeveloperName__c = 'ClosedWonOpportunities'; + reportTrigger.Active__c = true; + reportTrigger.DeviceID__c = 'deviceId'; + reportTrigger.AccessToken__c = 'accessToken'; + reportTrigger.GroupingIndex__c = 0; + reportTrigger.AggregateIndexForPercent__c = 1; + reportTrigger.AggregateIndexForDuration__c = null; + reportTrigger.UniqueName__c = 'Test'; + insert reportTrigger; + Opportunity opp = new Opportunity(); + opp.Name = 'Test'; + opp.StageName = 'Open'; + opp.CloseDate = System.today(); + opp.Amount = 10000; + insert opp; + + // When + Test.startTest(); + LittleBitsService.runReportTriggers(new Set { reportTrigger.UniqueName__c }); + Test.stopTest(); + + // Then + System.assertEquals(true, mocksLittleBitsCloudAPI.calloutMade); + } + @IsTest(SeeAllData=true) // Why SeeAllData? https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_analytics_test_reports.htm private static void callRunReportTriggersEnsureAExceptionOccurs() { @@ -164,6 +201,7 @@ private class LittleBitsServiceTest { reportTrigger.GroupingIndex__c = 0; reportTrigger.AggregateIndexForPercent__c = 1; reportTrigger.AggregateIndexForDuration__c = null; + reportTrigger.UniqueName__c = 'Test'; insert reportTrigger; // When ... Then diff --git a/src/classes/LittleBitsSubscriberInboundEmailHandler.cls b/src/classes/LittleBitsSubscriberInboundEmailHandler.cls new file mode 100644 index 0000000..84496e1 --- /dev/null +++ b/src/classes/LittleBitsSubscriberInboundEmailHandler.cls @@ -0,0 +1,33 @@ +/** + * Copyright (c), Cory Cowgill and Andrew Fawcett + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Cory Cowgill and Andrew Fawcett, nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +global with sharing class LittleBitsSubscriberInboundEmailHandler implements Messaging.InboundEmailHandler { + global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) { + Messaging.InboundEmailResult result = new Messaging.InboundEmailresult(); + LittleBitsService.processSubscriptionEvent(email); + return result; + } +} \ No newline at end of file diff --git a/src/classes/LittleBitsSubscriberInboundEmailHandler.cls-meta.xml b/src/classes/LittleBitsSubscriberInboundEmailHandler.cls-meta.xml new file mode 100644 index 0000000..7df4d0b --- /dev/null +++ b/src/classes/LittleBitsSubscriberInboundEmailHandler.cls-meta.xml @@ -0,0 +1,4 @@ + + + 31.0 + \ No newline at end of file diff --git a/src/layouts/LittleBitsDeviceSubscription__c-LittleBits Device Subscription Layout.layout b/src/layouts/LittleBitsDeviceSubscription__c-LittleBits Device Subscription Layout.layout index 8867df7..a21b367 100644 --- a/src/layouts/LittleBitsDeviceSubscription__c-LittleBits Device Subscription Layout.layout +++ b/src/layouts/LittleBitsDeviceSubscription__c-LittleBits Device Subscription Layout.layout @@ -74,7 +74,12 @@ FlowName__c - + + + Edit + RunAsUser__c + + diff --git a/src/layouts/LittleBitsReportTrigger__c-LittleBits Report Trigger Layout.layout b/src/layouts/LittleBitsReportTrigger__c-LittleBits Report Trigger Layout.layout index 53647e0..008ecba 100644 --- a/src/layouts/LittleBitsReportTrigger__c-LittleBits Report Trigger Layout.layout +++ b/src/layouts/LittleBitsReportTrigger__c-LittleBits Report Trigger Layout.layout @@ -12,6 +12,10 @@ Required Name + + Required + UniqueName__c + diff --git a/src/objects/LittleBitsDeviceSubscription__c.object b/src/objects/LittleBitsDeviceSubscription__c.object index d4c2bf1..1494ea3 100644 --- a/src/objects/LittleBitsDeviceSubscription__c.object +++ b/src/objects/LittleBitsDeviceSubscription__c.object @@ -134,6 +134,18 @@ Text false + + RunAsUser__c + SetNull + false + User must be the Context User for an active Inbound Email Service setup in this org called LittleBitsSubscriberInboundEmailHandler and using the LittleBitsSubscriberInboundEmailHandler class from this package. + + User + LittleBits_Device_Subscriptions + false + false + Lookup + All diff --git a/src/objects/LittleBitsReportTrigger__c.object b/src/objects/LittleBitsReportTrigger__c.object index 8898a66..8ced77b 100644 --- a/src/objects/LittleBitsReportTrigger__c.object +++ b/src/objects/LittleBitsReportTrigger__c.object @@ -130,6 +130,17 @@ Text false + + UniqueName__c + false + false + + 32 + true + false + Text + true + All diff --git a/src/permissionsets/LittleBitsConnect.permissionset b/src/permissionsets/LittleBitsConnect.permissionset index 97da04e..0720299 100644 --- a/src/permissionsets/LittleBitsConnect.permissionset +++ b/src/permissionsets/LittleBitsConnect.permissionset @@ -24,6 +24,11 @@ LittleBitsDeviceSubscription__c.FlowName__c true + + true + LittleBitsDeviceSubscription__c.RunAsUser__c + true + true LittleBitsReportTrigger__c.AccessToken__c