Building a Configurable Filter that Ignores Records Based on User-selected Cutoff Date

Here we’ll take a slightly more advanced scenario and walk through it.

Desired Behavior

  • A User can apply a condition to an incoming date field where that field is evaluated and if it fails our test, the record is ignored.

  • The test will be comparing the record’s date in the field to a cutoff/threshold date that has been configured by the User.

  • A User should be able to apply this condition to any incoming date field, and (if desired) more than one date field on the same record.

  • The cutoff dates are configured per-mapping, so if multiple fields should be inspected they can have different cutoff dates.

Solution Walkthrough

To satisfy these expectations we’ll want to create a custom Filter that implements both TransformationFilter and ConfigurablePerMappingFilter.

Our configuration needs are pretty simple (just a date picker), so we’ll use the Configuration Structure pattern for handling configurations.

Class Declaration

We start out with our class declaration and implementing both of our interfaces.

1/**
2 * Valence filter that allows us to set a date threshold that will cause records to be ignored if a given
3 * field from that record is older than our threshold.
4 */
5global with sharing class IgnoreOldRecordsFilter implements valence.TransformationFilter, valence.ConfigurablePerMappingFilter {

Warning

Make sure you declare your class as global, otherwise Valence won’t be able to see it and use it!

Configuration Setup

Since we opted for a configuration structure, we’ll be returning null from getMappingConfigurationLightningComponent().

The shape we return from getMappingConfigurationStructure() will be used by Valence to build a UI on our behalf and show it to the User. We are going to use the “date” flavor of lightning:input by setting the “type” attribute on that base component so that our User gets a nice, friendly date picker.

Valence will save the User-selected date to the database for us, and we’ll be given the value back later when we need it.

 7    public String getMappingConfigurationLightningComponent() {
 8        return null;
 9    }
10
11    public String getMappingConfigurationStructure() {
12		return DynamicUIConfigurationBuilder.create('Select a date below. Any records that have a value in this mapping older than the selected date will be ignored (exact date matches are not ignored).')
13			.addField(
14				DynamicUIConfigurationBuilder.createField('cutoff')
15				.addAttribute('label', 'Cutoff Date')
16				.addAttribute('type', 'date')
17			)
18			.finalize();
19    }

Tip

We don’t have to set “componentType” on the cutoff field because lightning:input is the default component type.

Configuration Explanation

We always want to give the User useful information about what they’ve done and what they can expect. Valence uses explainMappingConfiguration() to give us an opportunity to interpret a configuration and break it down in plain language for the User.

21    public String explainMappingConfiguration(String configuration) {
22
23        String explanation = 'This Filter will set records to be ignored if their field value (the one in this mapping) is older than {0}.';
24
25        Configuration config = (Configuration)JSON.deserialize(configuration, IgnoreOldRecordsFilter.Configuration.class);
26
27        return String.format(explanation, new List<String>{String.valueOf(config.cutoff)});
28    }

Configuration Class

For convenience and cleanliness it’s a good idea to create a simple inner Apex class to hold your configuration structure. Valence serializes configuration values from the form the User filled out into a JSON object whose keys are the name values you specified in your configuration schema. In our case we defined a single field called cutoff that we expect to find a serialized Date value inside.

70    /**
71     * Simple class for holding the configuration needed for this filter.
72     */
73    private class Configuration {
74        private Date cutoff;

Restricting Filter Usage

Some Filters only make sense in specific scenarios, for example RelationshipFilter (the built-in Valence Filter that handles populating Master-Detail and Lookup fields) only makes sense for records flowing into Salesforce, not outbound.

For this cutoff Filter we are building, we aren’t going to restrict it to only certain Links. All Links can use it.

30    public Boolean validFor(valence.LinkContext context) {
31        return true;
32    }

Processing Records

Finally, we get into the core purpose of our Filter: ignoring old records. Let’s walk through our process() method.

  1. Set up a Map we will use to line up the names of the record fields we’re going to inspect with the configured cutoff date for each field.

34    public void process(valence.LinkContext context, List<valence.RecordInFlight> records) {
35
36        Map<String, Date> cutoffsBySourceField = new Map<String, Date>();
  1. Iterate through the Mapping instances we are given as part of the LinkContext. Remember that Valence is clever here and will inject serialized User configurations from the database into mapping.configuration properties wherever the User has set up a configuration.

  2. We collect the cutoff Date values from a deserialized Configuration instance for any populated configurations.

41        for(valence.Mapping mapping : context.mappings.values()) {
42
43            // skip blank configurations
44            if(String.isNotBlank(mapping.configuration)) {
45                Configuration config = (Configuration)JSON.deserialize(mapping.configuration, IgnoreOldRecordsFilter.Configuration.class);
46                cutoffsBySourceField.put(mapping.sourceFieldName, config.cutoff);
47            }
48        }
  1. Now that we’ve assembled our Map, if it’s empty we can stop processing.

50        // bail out if we didn't find any
51		if(cutoffsBySourceField.isEmpty()) {
52			return;
  1. Now we iterate through the incoming RecordInFlight instances.

58        for(valence.RecordInFlight record : records) {
  1. For each field we need to check, inspect that field’s value for this record.

59            for(String sourceField : cutoffsBySourceField.keySet()) {
60                Date cutoff = cutoffsBySourceField.get(sourceField);
  1. Compare the field value to our cutoff date for this field. If older than the cutoff, mark this record as ignored.

61                Long fieldValue = (Long)record.getOriginalProperties().get(sourceField);
62                if(fieldValue != null) {
63                    if(Datetime.newInstance(fieldValue).dateGmt() < cutoff) {
64                        record.ignore('Record field <' + sourceField + '> older than cutoff of ' + cutoff);

Hint

You can see in the code for this example scenario we are assuming that all dates are being transmitted as Long values, i.e. milliseconds since Epoch. This is a simplification and you may not be able to make this same assumption in your real-world scenario!

Full Solution Code

Here is the complete solution code that we walked through above.

 1/**
 2 * Valence filter that allows us to set a date threshold that will cause records to be ignored if a given
 3 * field from that record is older than our threshold.
 4 */
 5global with sharing class IgnoreOldRecordsFilter implements valence.TransformationFilter, valence.ConfigurablePerMappingFilter {
 6
 7    public String getMappingConfigurationLightningComponent() {
 8        return null;
 9    }
10
11    public String getMappingConfigurationStructure() {
12		return DynamicUIConfigurationBuilder.create('Select a date below. Any records that have a value in this mapping older than the selected date will be ignored (exact date matches are not ignored).')
13			.addField(
14				DynamicUIConfigurationBuilder.createField('cutoff')
15				.addAttribute('label', 'Cutoff Date')
16				.addAttribute('type', 'date')
17			)
18			.finalize();
19    }
20
21    public String explainMappingConfiguration(String configuration) {
22
23        String explanation = 'This Filter will set records to be ignored if their field value (the one in this mapping) is older than {0}.';
24
25        Configuration config = (Configuration)JSON.deserialize(configuration, IgnoreOldRecordsFilter.Configuration.class);
26
27        return String.format(explanation, new List<String>{String.valueOf(config.cutoff)});
28    }
29
30    public Boolean validFor(valence.LinkContext context) {
31        return true;
32    }
33
34    public void process(valence.LinkContext context, List<valence.RecordInFlight> records) {
35
36        Map<String, Date> cutoffsBySourceField = new Map<String, Date>();
37
38        /*
39         * Assemble any cutoffs that have been configured by admins.
40         */
41        for(valence.Mapping mapping : context.mappings.values()) {
42
43            // skip blank configurations
44            if(String.isNotBlank(mapping.configuration)) {
45                Configuration config = (Configuration)JSON.deserialize(mapping.configuration, IgnoreOldRecordsFilter.Configuration.class);
46                cutoffsBySourceField.put(mapping.sourceFieldName, config.cutoff);
47            }
48        }
49
50        // bail out if we didn't find any
51		if(cutoffsBySourceField.isEmpty()) {
52			return;
53		}
54
55        /*
56         * Iterate through our records, ignoring where appropriate based on cutoff dates.
57         */
58        for(valence.RecordInFlight record : records) {
59            for(String sourceField : cutoffsBySourceField.keySet()) {
60                Date cutoff = cutoffsBySourceField.get(sourceField);
61                Long fieldValue = (Long)record.getOriginalProperties().get(sourceField);
62                if(fieldValue != null) {
63                    if(Datetime.newInstance(fieldValue).dateGmt() < cutoff) {
64                        record.ignore('Record field <' + sourceField + '> older than cutoff of ' + cutoff);
65                    }
66                }
67            }
68        }
69    }
70
71    /**
72     * Simple class for holding the configuration needed for this filter.
73     */
74    private class Configuration {
75        private Date cutoff;
76    }
77}