diff --git a/force-app/main/classes/DataStore.cls b/force-app/main/classes/DataStore.cls index 0630a26..305d83f 100644 --- a/force-app/main/classes/DataStore.cls +++ b/force-app/main/classes/DataStore.cls @@ -1,22 +1,31 @@ public class DataStore implements DataStoreInterface { + private Map> versionedDataMap; + public DataStore() { } - public VersionedData__C getVersioned(String key, String kind) { - List versioned = [ - SELECT Version__c, Raw__c, Key__c, Kind__c - FROM VersionedData__c - WHERE Key__c = :key AND Kind__c = :kind - LIMIT 1 - ]; - - if (versioned.isEmpty()) { - return null; - } else { - return versioned.get(0); + private void populateVersioned() { + // Preserve SOQL queries (each transaction only gets 100) by querying all data once + // Data can them be retrieved via the map structure in the event that multiple data are needed at different times + if (versionedDataMap == null) { + versionedDataMap = new Map>{ + 'flags' => new Map(), + 'segments' => new Map() + }; + for (VersionedData__c vd : [ + SELECT Id, Version__c, Raw__c, Key__c, Kind__c + FROM VersionedData__c + ]) { + versionedDataMap.get(vd.Kind__c).put(vd.Key__c, vd); + } } } + public VersionedData__c getVersioned(String key, String kind) { + populateVersioned(); + return versionedDataMap.get(kind).get(key); + } + public DataModel.Flag getFlag(String key) { VersionedData__c versioned = getVersioned(key, 'flags'); @@ -38,15 +47,10 @@ public class DataStore implements DataStoreInterface { } public Map allFlags() { + populateVersioned(); Map result = new Map(); - List flags = [ - SELECT Version__c, Raw__c, Key__c, Kind__c - FROM VersionedData__c - WHERE Kind__c = 'flags' - ]; - - for (VersionedData__C flag : flags) { + for (VersionedData__C flag : versionedDataMap.get('flags').values()) { try { result.put(flag.Key__c, new DataModel.Flag(flag.Raw__c)); } catch (Exception err) { @@ -59,9 +63,15 @@ public class DataStore implements DataStoreInterface { public void putAll(Map kinds) { // delete existing store values - List existingFeatures = [SELECT Key__c FROM VersionedData__C]; - delete existingFeatures; + populateVersioned(); + List existingFeatures = new List(); + existingFeatures.addAll(versionedDataMap.get('flags').values()); + existingFeatures.addAll(versionedDataMap.get('segments').values()); + if (existingFeatures.size() > 0) { + delete existingFeatures; + } // iterate over kinds of features such as flags / segments + List featureList = new List(); for (String kind : kinds.keySet()) { Map features = (Map) kinds.get(kind); @@ -70,16 +80,14 @@ public class DataStore implements DataStoreInterface { VersionedData versioned = new VersionedData(kind, feature); - this.insertVersionedData(versioned); + featureList.add(versioned.getSObject()); } } - } - - public void insertVersionedData(VersionedData value) { - try { - insert value.getSObject(); - } catch (Exception err) { - // required by compiler + if (featureList.size() > 0) { + insert featureList; } + + // clear our versionedDataMap so that next time it is required it will be repopulated + versionedDataMap = null; } } diff --git a/force-app/main/classes/EventSink.cls b/force-app/main/classes/EventSink.cls index eb766c5..73bc9a0 100644 --- a/force-app/main/classes/EventSink.cls +++ b/force-app/main/classes/EventSink.cls @@ -1,10 +1,13 @@ public class EventSink implements EventSinkInterface { Integer maxEvents; + private Integer eventsAddedCount; + private Integer existingEventsCount; public EventSink(Integer maxEvents) { System.assertNotEquals(maxEvents, null); this.maxEvents = maxEvents; + this.eventsAddedCount = 0; } public void sinkIdentify(LDEvent.Identify event) { @@ -24,9 +27,12 @@ public class EventSink implements EventSinkInterface { } public void sinkGeneric(String kind, String raw) { - Integer count = [SELECT COUNT() FROM EventData__c]; - - if (count >= this.maxEvents) { + // Preserve SOQL queries (each transaction only gets 100) by getting count once and comparing to updated events count + if (existingEventsCount == null) { + existingEventsCount = [SELECT COUNT() FROM EventData__c]; + } + // Only create event if threshold has not been met + if ((existingEventsCount + eventsAddedCount) >= this.maxEvents) { return; } @@ -35,5 +41,6 @@ public class EventSink implements EventSinkInterface { record.Raw__c = raw; insert record; + eventsAddedCount++; } } diff --git a/force-app/main/classes/LDClientInvocable.cls b/force-app/main/classes/LDClientInvocable.cls new file mode 100644 index 0000000..5182116 --- /dev/null +++ b/force-app/main/classes/LDClientInvocable.cls @@ -0,0 +1,134 @@ +public class LDClientInvocable { + + public class InvocableRequest { + @InvocableVariable(label='Flag Key' description='Flag Key as defined in LaunchDarkly' required=true) + public String flagKey; + @InvocableVariable(label='Flag Type' description='Flag Type corresponding to Flag Key. Valid options are: Boolean, Double, Integer, and String.' required=true) + public String flagType; + @InvocableVariable(label='Fallback (Boolean)' description='Fallback for Boolean Flag type if no variation found from LaunchDarkly' required=false) + public Boolean booleanFallback; + @InvocableVariable(label='Fallback (Double)' description='Fallback for Double (Number) Flag type if no variation found from LaunchDarkly' required=false) + public Double doubleFallback; + @InvocableVariable(label='Fallback (Integer)' description='Fallback for Integer (Number) Flag type if no variation found from LaunchDarkly' required=false) + public Integer integerFallback; + @InvocableVariable(label='Fallback (String)' description='Fallback for String Flag type if no variation found from LaunchDarkly' required=false) + public String stringFallback; + @InvocableVariable(label='LaunchDarkly Client' description='If provided, the LD Client can be persisted across requests to preserve SOQL queries and DML statements' required=false) + public LDFlowClient ldClient_Flow; + @InvocableVariable(label='User custom attributes' description='Key/Value pairs to assign to user context evaluation. Id and Name will be assigned automatically' required=false) + public List userCustomAttributes; + @InvocableVariable(label='User to be Identified' description='If TRUE, the LDClient.identify method will be called' required=false) + public Boolean userToBeIdentified; + } + + public class InvocableResponse { + @InvocableVariable(label='LaunchDarkly Client' description='Instance of LDClient used to retrieve the variation' required=false) + public LDFlowClient ldClient_Flow; + @InvocableVariable(label='Variation (Boolean)' description='Variation for Boolean Flag type' required=false) + public Boolean booleanVariation; + @InvocableVariable(label='Variation (Double)' description='Variation for Double (Number) Flag type' required=false) + public Double doubleVariation; + @InvocableVariable(label='Variation (Integer)' description='Variation for Integer (Number) Flag type' required=false) + public Integer integerVariation; + @InvocableVariable(label='Variation (String)' description='Variation for String Flag type' required=false) + public String stringVariation; + + public InvocableResponse(InvocableRequest request) { + this.booleanVariation = false; + } + } + + @InvocableMethod(label='Get LaunchDarkly Variation' description='Returns the flag variation for a given of Flag Key') + public static List flagVariation(List requests) { + LDClient client = determineClient(requests); + LDFlowClient flowClient = new LDFlowClient(); + flowClient.client = client; + List results = new List(); + for (InvocableRequest request : requests) { + LDUser user = buildLDUser(client, request); + InvocableResponse result = new InvocableResponse(request); + result.ldClient_Flow = flowClient; + if (request.flagType != null) { + switch on request.flagType.toLowerCase() { + when 'boolean' { + result.booleanVariation = client.boolVariation(user, request.flagKey, request.booleanFallback != null ? request.booleanFallback : false); + } + when 'double' { + result.doubleVariation = client.doubleVariation(user, request.flagKey, request.doubleFallback); + } + when 'integer' { + result.integerVariation = client.intVariation(user, request.flagKey, request.integerFallback); + } + when 'string' { + result.stringVariation = client.stringVariation(user, request.flagKey, request.stringFallback); + } + when else { + } + } + } + results.add(result); + } + return results; + } + + private static LDClient determineClient(List requests) { + LDClient client; + for (InvocableRequest request : requests) { + if (client != null) { + break; + } + if (request.ldClient_Flow != null) { + client = request.ldClient_Flow.client; + } + } + if (client == null) { + client = new LDClient(); + } + + return client; + } + + private static LDUser buildLDUser(LDClient client, InvocableRequest request) { + Map valueObjectMap = new Map(); + if (request.userCustomAttributes != null) { + for (LDFlowMapKey userAttribute : request.userCustomAttributes) { + if (userAttribute.valueType != null) { + switch on userAttribute.valueType.toLowerCase() { + when 'boolean' { + if (userAttribute.booleanValue != null) { + valueObjectMap.put(userAttribute.key, LDValue.of(userAttribute.booleanValue)); + } + } + when 'double' { + if (userAttribute.doubleValue != null) { + valueObjectMap.put(userAttribute.key, LDValue.of(userAttribute.doubleValue)); + } + } + when 'integer' { + if (userAttribute.integerValue != null) { + valueObjectMap.put(userAttribute.key, LDValue.of(userAttribute.integerValue)); + } + } + when 'string' { + if (userAttribute.stringValue != null) { + valueObjectMap.put(userAttribute.key, LDValue.of(userAttribute.stringValue)); + } + } + when else { + } + } + } + } + } + LDUser user = new LDUser.Builder(System.UserInfo.getUserId()) + .setName(System.UserInfo.getName()) + .setCustom(LDValueObject.fromMap(valueObjectMap)) + .build(); + + if (request.userToBeIdentified == true) { + client.identify(user); + } + + return user; + } +} diff --git a/force-app/main/classes/LDClientInvocable.cls-meta.xml b/force-app/main/classes/LDClientInvocable.cls-meta.xml new file mode 100644 index 0000000..bab2ccd --- /dev/null +++ b/force-app/main/classes/LDClientInvocable.cls-meta.xml @@ -0,0 +1,5 @@ + + + 49.0 + Active + diff --git a/force-app/main/classes/LDFlowClient.cls b/force-app/main/classes/LDFlowClient.cls new file mode 100644 index 0000000..1f1ed86 --- /dev/null +++ b/force-app/main/classes/LDFlowClient.cls @@ -0,0 +1,10 @@ +// Used as an Apex-Defined Variable to allow LDClient persistance across variation calls +public class LDFlowClient { + @AuraEnabled + public LDClient client; + // clientName only included so that LDFlowClient is visible as an Apex-Defined Variable in the Flow + @AuraEnabled + public String clientName; + + public LDFlowClient(){} +} \ No newline at end of file diff --git a/force-app/main/classes/LDFlowClient.cls-meta.xml b/force-app/main/classes/LDFlowClient.cls-meta.xml new file mode 100644 index 0000000..bab2ccd --- /dev/null +++ b/force-app/main/classes/LDFlowClient.cls-meta.xml @@ -0,0 +1,5 @@ + + + 49.0 + Active + diff --git a/force-app/main/classes/LDFlowMapKey.cls b/force-app/main/classes/LDFlowMapKey.cls new file mode 100644 index 0000000..9ef715f --- /dev/null +++ b/force-app/main/classes/LDFlowMapKey.cls @@ -0,0 +1,17 @@ +// Used as an Apex-Defined Variable to allow map-like entry for user custom attributes +public class LDFlowMapKey { + @AuraEnabled + public string key; + @AuraEnabled + public string valueType; + @AuraEnabled + public Boolean booleanValue; + @AuraEnabled + public Double doubleValue; + @AuraEnabled + public Integer integerValue; + @AuraEnabled + public string stringValue; + + public LDFlowMapKey(){} +} \ No newline at end of file diff --git a/force-app/main/classes/LDFlowMapKey.cls-meta.xml b/force-app/main/classes/LDFlowMapKey.cls-meta.xml new file mode 100644 index 0000000..bab2ccd --- /dev/null +++ b/force-app/main/classes/LDFlowMapKey.cls-meta.xml @@ -0,0 +1,5 @@ + + + 49.0 + Active + diff --git a/force-app/test/LDClientInvocableTest.cls b/force-app/test/LDClientInvocableTest.cls new file mode 100644 index 0000000..2fa3d05 --- /dev/null +++ b/force-app/test/LDClientInvocableTest.cls @@ -0,0 +1,85 @@ +@isTest +private class LDClientInvocableTest { + + @isTest + static void basicVariation() { + LDClientInvocable.InvocableRequest requestData = new LDClientInvocable.InvocableRequest(); + requestData.flagKey = 'fakeFlag'; + requestData.flagType = 'Boolean'; + requestData.booleanFallback = false; + requestData.userCustomAttributes = new List(); + + LDFlowMapKey booleanMapKey = new LDFlowMapKey(); + booleanMapKey.key = 'booleanKey'; + booleanMapKey.valueType = 'Boolean'; + booleanMapKey.booleanValue = false; + requestData.userCustomAttributes.add(booleanMapKey); + + LDFlowMapKey doubleMapKey = new LDFlowMapKey(); + doubleMapKey.key = 'doubleKey'; + doubleMapKey.valueType = 'Double'; + doubleMapKey.doubleValue = 25.48; + requestData.userCustomAttributes.add(doubleMapKey); + + LDFlowMapKey integerMapKey = new LDFlowMapKey(); + integerMapKey.key = 'integerKey'; + integerMapKey.valueType = 'Integer'; + integerMapKey.integerValue = 25; + requestData.userCustomAttributes.add(integerMapKey); + + LDFlowMapKey stringMapKey = new LDFlowMapKey(); + stringMapKey.key = 'stringKey'; + stringMapKey.valueType = 'String'; + stringMapKey.stringValue = 'string'; + requestData.userCustomAttributes.add(stringMapKey); + + requestData.userToBeIdentified = true; + + LDClientInvocable.InvocableResponse responseData = LDClientInvocable.flagVariation(new List{requestData})[0]; + + System.assertEquals(false, responseData.booleanVariation, 'Response Boolean Variation should be FALSE to match the fallback value since the flag does not exist'); + } + + + @isTest + static void intVariation() { + LDClientInvocable.InvocableRequest requestData = new LDClientInvocable.InvocableRequest(); + requestData.flagKey = 'fakeFlag'; + requestData.flagType = 'Integer'; + requestData.integerFallback = 2024; + requestData.userCustomAttributes = new List(); + requestData.userToBeIdentified = true; + + LDClientInvocable.InvocableResponse responseData = LDClientInvocable.flagVariation(new List{requestData})[0]; + + System.assertEquals(2024, responseData.integerVariation, 'Response Integer Variation should be 2024 to match the fallback value since the flag does not exist'); + } + + @isTest + static void doubleVariation() { + LDClientInvocable.InvocableRequest requestData = new LDClientInvocable.InvocableRequest(); + requestData.flagKey = 'fakeFlag'; + requestData.flagType = 'Double'; + requestData.doubleFallback = 2024.06; + requestData.userCustomAttributes = new List(); + requestData.userToBeIdentified = true; + + LDClientInvocable.InvocableResponse responseData = LDClientInvocable.flagVariation(new List{requestData})[0]; + + System.assertEquals(2024.06, responseData.doubleVariation, 'Response Double Variation should be 2024.06 to match the fallback value since the flag does not exist'); + } + + @isTest + static void stringVariation() { + LDClientInvocable.InvocableRequest requestData = new LDClientInvocable.InvocableRequest(); + requestData.flagKey = 'fakeFlag'; + requestData.flagType = 'String'; + requestData.stringFallback = 'test string'; + requestData.userCustomAttributes = new List(); + requestData.userToBeIdentified = true; + + LDClientInvocable.InvocableResponse responseData = LDClientInvocable.flagVariation(new List{requestData})[0]; + + System.assertEquals('test string', responseData.stringVariation, 'Response String Variation should be \'test string\' to match the fallback value since the flag does not exist'); + } +} diff --git a/force-app/test/LDClientInvocableTest.cls-meta.xml b/force-app/test/LDClientInvocableTest.cls-meta.xml new file mode 100644 index 0000000..bab2ccd --- /dev/null +++ b/force-app/test/LDClientInvocableTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 49.0 + Active +