Skip to content

Moonshots XXI allow for use in declarative sfdc #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 37 additions & 29 deletions force-app/main/classes/DataStore.cls
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
public class DataStore implements DataStoreInterface {
private Map<String, Map<String, VersionedData__c>> versionedDataMap;

public DataStore() {
}

public VersionedData__C getVersioned(String key, String kind) {
List<VersionedData__c> 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<String, Map<String, VersionedData__c>>{
'flags' => new Map<String, VersionedData__c>(),
'segments' => new Map<String, VersionedData__c>()
};
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');

Expand All @@ -38,15 +47,10 @@ public class DataStore implements DataStoreInterface {
}

public Map<String, DataModel.Flag> allFlags() {
populateVersioned();
Map<String, DataModel.Flag> result = new Map<String, DataModel.Flag>();

List<VersionedData__c> 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) {
Expand All @@ -59,9 +63,15 @@ public class DataStore implements DataStoreInterface {

public void putAll(Map<String, Object> kinds) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

putAll should have a comment saying that it's just used for the side-effect of updating the underlying data tables, and it's not going to have any effect on the Data Store instances used by SDK clients

// delete existing store values
List<VersionedData__c> existingFeatures = [SELECT Key__c FROM VersionedData__C];
delete existingFeatures;
populateVersioned();
List<VersionedData__c> existingFeatures = new List<VersionedData__c>();
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<VersionedData__c> featureList = new List<VersionedData__c>();
for (String kind : kinds.keySet()) {
Map<String, Object> features = (Map<String, Object>) kinds.get(kind);

Expand All @@ -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;
}
}
13 changes: 10 additions & 3 deletions force-app/main/classes/EventSink.cls
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: These instances all shared EventData__c (?). If so, it's not safe to query the existing events count once because we could have a race condition where multiple instances think it's safe to add new events, but it is not.

existingEventsCount = [SELECT COUNT() FROM EventData__c];
}
// Only create event if threshold has not been met
if ((existingEventsCount + eventsAddedCount) >= this.maxEvents) {
return;
}

Expand All @@ -35,5 +41,6 @@ public class EventSink implements EventSinkInterface {
record.Raw__c = raw;

insert record;
eventsAddedCount++;
}
}
134 changes: 134 additions & 0 deletions force-app/main/classes/LDClientInvocable.cls
Original file line number Diff line number Diff line change
@@ -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<LDFlowMapKey> 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this actually be null?

@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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why is this being set?

}
}

@InvocableMethod(label='Get LaunchDarkly Variation' description='Returns the flag variation for a given of Flag Key')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@InvocableMethod(label='Get LaunchDarkly Variation' description='Returns the flag variation for a given of Flag Key')
@InvocableMethod(label='Get LaunchDarkly Variation' description='Returns the flag variation for a given Flag Key')

public static List<InvocableResponse> flagVariation(List<InvocableRequest> requests) {
LDClient client = determineClient(requests);
LDFlowClient flowClient = new LDFlowClient();
flowClient.client = client;
List<InvocableResponse> results = new List<InvocableResponse>();
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<InvocableRequest> requests) {
LDClient client;
for (InvocableRequest request : requests) {
if (client != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just return on line 81?

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<String, LDValue> valueObjectMap = new Map<String, LDValue>();
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

}
}
}
}
}
LDUser user = new LDUser.Builder(System.UserInfo.getUserId())
.setName(System.UserInfo.getName())
.setCustom(LDValueObject.fromMap(valueObjectMap))
.build();

if (request.userToBeIdentified == true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (request.userToBeIdentified == true) {
if (request.userToBeIdentified) {

client.identify(user);
}

return user;
}
}
5 changes: 5 additions & 0 deletions force-app/main/classes/LDClientInvocable.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ApexClass xmlns="urn:metadata.tooling.soap.sforce.com" fqn="OrderController">
<apiVersion>49.0</apiVersion>
<status>Active</status>
</ApexClass>
10 changes: 10 additions & 0 deletions force-app/main/classes/LDFlowClient.cls
Original file line number Diff line number Diff line change
@@ -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(){}
}
5 changes: 5 additions & 0 deletions force-app/main/classes/LDFlowClient.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ApexClass xmlns="urn:metadata.tooling.soap.sforce.com" fqn="OrderController">
<apiVersion>49.0</apiVersion>
<status>Active</status>
</ApexClass>
17 changes: 17 additions & 0 deletions force-app/main/classes/LDFlowMapKey.cls
Original file line number Diff line number Diff line change
@@ -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(){}
}
5 changes: 5 additions & 0 deletions force-app/main/classes/LDFlowMapKey.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ApexClass xmlns="urn:metadata.tooling.soap.sforce.com" fqn="OrderController">
<apiVersion>49.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading