Skip to main content

Apex SDK documentation

The InCountry Apex SDK allows users of the InCountry Data Residency for Salesforce package to make CRUD (create/read/update/delete) operations with their regulated data stored in InCountry PoP's (Points of Presence). The Apex SDK is provided as a part of the InCountry Data Residency for the Salesforce app. All the SDK components are defined as global Apex classes. That’s why the SDK can be used by the app subscribers within their custom code solutions.

note

The InCountry platdorm uses the testInCountry1 namespace prefix so please make sure you use this prefix when initiating any of the APEX SDK methods.

For example: testInCountry1.InCountry.getInstance();

Apex SDK Lightning UI

Apex SDK Rest API

Supported data regulation models

The InCountry Apex SDK supports the replication and restriction data regulation models. The Apex SDK provides the following operations on protected data depending on the used data regulation model:

OperationReplicationRestrictionCallout
Finding recordsSupportedSupportedTrue
Writing recordsSupportedSupportedTrue
Writing multiple recordsSupportedSupportedTrue
Updating recordsNot supportedSupportedTrue
Synchronizing recordsNot supportedSupportedTrue
Synchronizing multiple recordsNot supportedSupportedTrue
Synchronizing records with data loss preventionNot supportedSupportedTrue
Deleting recordsSupportedSupportedTrue
Attaching filesNot supportedSupportedTrue
Deleting filesNot supportedSupportedTrue
Setting the time to live (TTL) for recordsSupportedSupportedTrue
Resetting the time to live (TTL) for recordsSupportedSupportedTrue
Check if records are regulatedSupportedSupportedFalse
Check if multiple records are regulatedSupportedSupportedFalse
Redacting recordsSupportedSupportedFalse
Redacting valuesSupportedSupportedFalse
Delete record historySupportedSupportedTrue
Redacting values in a Salesforce objectSupportedSupportedFalse
Execute business logic after record create/update on InCountry Record DetailSupportedSupportedTrue
Check if records are syncedNot supportedSupportedTrue

Internally, the Apex SDK automatically differentiates records by country and makes callouts to InCountry Rest API.

note

InCountry Rest API endpoints must be registered within the InCountryRestAPIEndpoint custom metadata type and added into the remote site settings to allow server-side callouts. This guide does not cover the initial setup and configuration of InCountry Data Residency for Salesforce. You can find all the relevant information about this at our documentation site.

Quick Start Guide

To access operations available in InCountry PoP, you need to create an instance of the InCountry class. You should prepend the InCountry Managed Package namespace to the class name, like <namespace>.InCountry. For example, if the namespace of the InCountry Data Residency for Salesforce package is testIncountry1, you need to use the following class name: testIncountry1.InCountry.

note

All the following code samples below are provided without a namespace. Please prepend the InCountry Namespace to class names.

How to instantiate the Apex SDK

You need to execute the static getInstance() method from the InCountry class. It returns an instance of the Apex SDK.

InCountry service = InCountry.getInstance();

How to find records stored in the InCountry platform

You can find records stored in the InCountry platform by using two different approaches:

  1. by records ids;

  2. by records instances.

The Apex SDK will automatically split records by country and make callouts to appropriate InCountry Points of Presence located in each target country. For example, if you want to fetch two leads stored in InCountry Sweden and one account stored in InCountry Russia, the Apex SDK will make two callouts: one callout to Sweden and the other callout to Russia.

Finding by records ids

In this scenario, you should provide a set of record ids. Please consider that the Apex SDK will perform additional SOQL queries to retrieve records from the Salesforce database. For example, if you provide three account Ids and five lead Ids, the SDK will perform two additional SOQL queries to retrieve accounts and leads from the database. These SOQL queries are required because the SDK should fetch all the regulated field values and the record originating countries from the database. In the following example, the SDK will fetch two leads and one account from the InCountry platform.

InCountry service = InCountry.getInstance();
Set<Id> input = new Set<Id>{'00Q5w00001vsLjVEAU', '00Q5w00001vrzjQEAQ', '0015w00002OUr9uAAD'};
List<InCountry.FindResult> results = service.find(input);

Finding by record instances

If you have queried all the necessary records from the Salesforce database, you can provide the record instances to the find() method so that the SDK will skip additional SOQL queries. Please ensure that all the required fields are included in your SOQL queries. The Apex SDK requires all the regulated fields and the Source Country Field field to use the record-level policy for determining the record originating country.

InCountry service = InCountry.getInstance();
List<Lead> input = [SELECT Id, FirstName, Email,
Phone, Country_Field__c
FROM Lead
ORDER BY CreatedDate DESC
LIMIT 10];
List<InCountry.FindResult> results = service.find(input);

The FindResult class contains data returned from the InCountry platform:

@JsonAccess(serializable='always' deserializable='always')
global class FindResult {
@AuraEnabled global List<RecordData> data;
@AuraEnabled global Map<String, String> meta;
}

The RecordData class represents a single Salesforce record stored in the InCountry platform:

@JsonAccess(serializable='always' deserializable='always')
global class RecordData {
@AuraEnabled global String key;
@AuraEnabled global String record_key;
@AuraEnabled global String profile_key;
@AuraEnabled global String range_key;
@AuraEnabled global String body;
@AuraEnabled global String key1;
@AuraEnabled global String key2;
@AuraEnabled global String key3;
@AuraEnabled global String key4;
@AuraEnabled global String key5;
@AuraEnabled global String key6;
@AuraEnabled global String key7;
@AuraEnabled global String key8;
@AuraEnabled global String key9;
@AuraEnabled global String key10;
@AuraEnabled global String key11;
@AuraEnabled global String key12;
@AuraEnabled global String key13;
@AuraEnabled global String key14;
@AuraEnabled global String key15;
@AuraEnabled global String key16;
@AuraEnabled global String key17;
@AuraEnabled global String key18;
@AuraEnabled global String key19;
@AuraEnabled global String key20;
@AuraEnabled global String created_at;
@AuraEnabled global String updated_at;
@AuraEnabled global String expires_at;
@AuraEnabled global List<FileData> attachments;
}
note

The @AuraEnabled global String expires_at; snippet is not available for the restriction data regulation model.

The FileData class represents a single file linked to a specific InCountry record:

@JsonAccess(serializable='always' deserializable='always')
global class FileData {
@AuraEnabled global String file_id;
@AuraEnabled global String filename;
@AuraEnabled global String hash;
@AuraEnabled global String mime_type;
@AuraEnabled global Integer size;
@AuraEnabled global String created_at;
@AuraEnabled global String updated_at;
@AuraEnabled global String download_link;
@AuraEnabled global String error;
}

How to write records into the InCountry platform

Like the find operation, you can use two different methods to write single Salesforce records into the InCountry platform.

Writing by record ids

Please consider that the Apex SDK will perform an additional SOQL query to fetch a salesforce database record. If the current user on behalf of whom an operation is executed has no sufficient permissions to the record id, an exception will be thrown.

InCountry service = InCountry.getInstance();
InCountry.WriteRequest result = service.write('00Q5w00001vrzjQEAQ');

Writing by record instance

Before executing the write(SObject record) method, ensure that you have included all the necessary regulated and country fields into your SOQL queries.

InCountry service = InCountry.getInstance();
Lead input = [SELECT Id, FirstName, Email, Phone, Country_Field__c
FROM Lead
LIMIT 1];
input.FirstName = 'ChangedValue';
InCountry.WriteRequest result = service.write(input);

The WriteRequest class represents a confirmation which the InCountry platform returns upon a successful execution of the write request.

@JsonAccess(serializable='always' deserializable='always')
global class WriteRequest {
@AuraEnabled global String country;
@AuraEnabled global String profile_key;
@AuraEnabled global String body;
@AuraEnabled global String record_key;
@AuraEnabled global String key;
@AuraEnabled global String key1;
@AuraEnabled global String key2;
@AuraEnabled global String key3;
@AuraEnabled global String key4;
@AuraEnabled global String key5;
@AuraEnabled global String key6;
@AuraEnabled global String key7;
@AuraEnabled global String key8;
@AuraEnabled global String key9;
@AuraEnabled global String key10;
@AuraEnabled global String key11;
@AuraEnabled global String key12;
@AuraEnabled global String key13;
@AuraEnabled global String key14;
@AuraEnabled global String key15;
@AuraEnabled global String key16;
@AuraEnabled global String key17;
@AuraEnabled global String key18;
@AuraEnabled global String key19;
@AuraEnabled global String key20;
@AuraEnabled global String expires_at;
}
note

The @AuraEnabled global String expires_at; snippet is not available for the restriction data regulation model.

How to write multiple records into the InCountry platform

The Apex SDK performs requests to a separate /api/records/batch endpoint in InCountry Rest API to write multiple records to the InCountry platform to perform batch write operations. The Apex SDK will automatically split records by country and will make several callouts to different InCountry Points of Presence in the target countries. For example, if you want to write two Leads to InCountry Sweden and one account to InCountry Russia, the Apex SDK will make two callouts, as follows: one callout to Sweden and the other callout to Russia.

Writing a batch of records by records ids

Please consider that InCountry SDK will perform additional SOQL queries to fetch records from the Salesforce database. If the current user on behalf of whom an operation is executed has no sufficient permissions to record id, an exception will be thrown.

For example, if you provide three account ids and five lead ids, the SDK will perform two additional SOQL queries to fetch accounts and leads from the database. These SOQL queries are required because the Apex SDK should take all the regulated field values and record originating countries from the database.

InCountry service = InCountry.getInstance();
Set<Id> input = new Set<Id>{'00Q5w00001vsLjVEAU', '00Q5w00001vrzjQEAQ', '0015w00002OUr9uAAD'};
List<InCountry.WriteRequest> result = service.write(input);

Writing a batch of records by record instances

Before executing the write(List<SObject> records) method, ensure that you have all the required regulated and country fields included in your SOQL queries.

InCountry service = InCountry.getInstance();
List<Lead> input = [SELECT Id, FirstName, Email, Phone, Country_Field__c
FROM Lead
LIMIT 40];
for (Lead leadRecord : input) {
leadRecord.Phone = '+37529000000';
}
List<InCountry.WriteRequest> result = service.write(input);

How to update multiple records on the InCountry platform

The Apex SDK performs requests to a separate /api/records/batch/update endpoint in InCountry Rest API to update multiple records to the InCountry platform. The Apex SDK will automatically split records by country and will make several callouts to different InCountry Points of Presence in the target countries.

Updating a batch of records by record instances

To update certain fields you have to add them in your SOQL request and then execute the update(List<SObject> records) method.

InCountry service = InCountry.getInstance();
List<Lead> input = [SELECT Id, Phone FROM Lead LIMIT 5];
for (Lead leadRecord : input) {
leadRecord.Phone = '+37529000000';
}
List<InCountry.WriteRequest> result = service.updateRecords(input);

How to synchronize single records with the InCountry platform

Like the find and writeoperations, you can use two different methods to synchronize single Salesforce records with the InCountry platform.

Synchronizing by record ids

Please consider that the Apex SDK will perform an additional SOQL query to fetch a record from the Salesforce database. If the current user on behalf of whom an operation is executed has no sufficient permissions to the record id, an exception will be thrown. Once the Apex SDK has written a data record into the InCountry platform, it will perform a DML query to update a record in the Salesforce database with hashed values.

InCountry service = InCountry.getInstance();
InCountry.WriteRequest result = service.sync('00Q5w00001vrzjQEAQ');

Synchronizing by record instance

Before executing the sync(SObject record) method, ensure that you have included all the necessary regulated and country fields into your SOQL queries. Once the Apex SDK has written a data record into the InCountry platform, it will perform a DML query to update a record in the Salesforce database with hashed values.

InCountry service = InCountry.getInstance();
Lead input = [SELECT Id, FirstName, Email, Phone, Country_Field__c
FROM Lead
LIMIT 1];
input.FirstName = 'ChangedValue';
InCountry.WriteRequest result = service.sync(input);

The WriteRequest class represents a confirmation which the InCountry platform returns upon a successful execution of the write request.

@JsonAccess(serializable='always' deserializable='always')
global class WriteRequest {
@AuraEnabled global String country;
@AuraEnabled global String profile_key;
@AuraEnabled global String body;
@AuraEnabled global String record_key;
@AuraEnabled global String key;
@AuraEnabled global String key1;
@AuraEnabled global String key2;
@AuraEnabled global String key3;
@AuraEnabled global String key4;
@AuraEnabled global String key5;
@AuraEnabled global String key6;
@AuraEnabled global String key7;
@AuraEnabled global String key8;
@AuraEnabled global String key9;
@AuraEnabled global String key10;
@AuraEnabled global String key11;
@AuraEnabled global String key12;
@AuraEnabled global String key13;
@AuraEnabled global String key14;
@AuraEnabled global String key15;
@AuraEnabled global String key16;
@AuraEnabled global String key17;
@AuraEnabled global String key18;
@AuraEnabled global String key19;
@AuraEnabled global String key20;
@AuraEnabled global String expires_at;
}

How to synchronize multiple records with the InCountry platform

The Apex SDK performs requests to a separate /api/records/batch endpoint in InCountry Rest API to write multiple records to the InCountry platform to perform batch write operations. The Apex SDK will automatically split records by country and will make several callouts to different InCountry Points-of-Presence in the target countries. For example, if you want to write two Leads to InCountry Sweden and one account to InCountry Russia, the Apex SDK will make two callouts, as follows: one callout to Sweden and the other callout to Russia. Once the Apex SDK has written data records into the InCountry platform it will perform A DML query to update records in the Salesforce database with hashed values.

Synchronizing multiple records by records ids

Please consider that the Apex SDK will perform additional SOQL queries to fetch records from the Salesforce database. If the current user on behalf of whom an operation is executed has no sufficient permissions to record id, an exception will be thrown.

For example, if you provide three account ids and five lead ids, the Apex SDK will perform two additional SOQL queries to fetch accounts and leads from the database. These SOQL queries are required because the Apex SDK should take all the regulated field values and record originating countries from the database.

InCountry service = InCountry.getInstance();
Set<Id> input = new Set<Id>{'00Q5w00001vsLjVEAU', '00Q5w00001vrzjQEAQ', '0015w00002OUr9uAAD'};
List<InCountry.WriteRequest> result = service.sync(input);

Synchronizing multiple records by record instances

Before executing the sync(List<SObject> records) method, ensure that you have all the required regulated and country fields included into your SOQL queries.

InCountry service = InCountry.getInstance();
List<Lead> input = [SELECT Id, FirstName, Email, Phone, Country_Field__c
FROM Lead
LIMIT 40];
for (Lead leadRecord : input) {
leadRecord.Phone = '+37529000000';
}
List<InCountry.WriteRequest> result = service.sync(input);

Synchronizing records with data loss prevention

There is a possible situation when records were not successfully synchronized to the InCountry platform but were hashed on the Salesforce side. In this case, the clear text data was lost. The script below shows how not to lose the clear text data if some records fail to synchronize to the InCountry platform. The records that failed to synchronize with the clear text values will be stored in the failedAccountsWithClearText variable.

List<Account> accountListToSync = [SELECT Id FROM Account];
Map<Id,Account> backupAccountMap = new Map<Id, Account>(accountListToSync);
List<Account> failedAccountsWithClearText = new List<Account>();

InCountry service = InCountry.getInstance();
List<InCountry.WriteRequest> response = service.sync(accountListToSync);
for (InCountry.WriteRequest request : response) {
if(String.isNotBlank(request.error)) {
failedAccountsWithClearText.add(backupAccountMap.get(request.profile_key));
}
}
system.debug(failedAccountsWithClearText);

How to remove records from the InCountry platform

As of now, the InCountry platform does not support batch delete operations. If you want to delete multiple records from the InCountry platform, please consider that the Apex SDK will perform a callout for each record.

Removing a single record by record id

Please consider that the Apex SDK will perform an additional SOQL query to fetch a record and package configuration from the Salesforce database.

InCountry service = InCountry.getInstance();
Id input = '0015w00002OUr9uAAD';
InCountry.DeleteResult result = service.remove(input);

Removing multiple records by records ids

Please consider that the Apex SDK will perform additional SOQL queries to fetch your records and package configuration from the Salesforce database. In the following example, the Apex SDK will perform two additional SOQL queries and three callouts.

InCountry service = InCountry.getInstance();
Set<Id> input = new Set<Id>{'00Q5w00001vsLjVEAU', '00Q5w00001vrzjQEAQ', '0015w00002OUr9uAAD'};
List<InCountry.DeleteResult> result = service.remove(input);

Removing multiple records by key

Please consider that the Apex SDK will perform an additional SOQL query to fetch package configuration from the Salesforce database. In the following example, the Apex SDK will perform one SOQL query and one callout for any number of records.

The result is applied to the whole operation. It means that if at least one record is found and deleted, the operation will be considered successful. If no records are found, the error message No records matching the filter is displayed.

The InCountry Apex SDK supports the following keys:

  • profile_key

  • record_key

  • parent_key

  • range_key

InCountry service = InCountry.getInstance();
Set<String> input = new Set<String>{'00Q0900000M9OXQEA3', '00Q0900000NG9lcEAD'};
InCountry.BatchDeleteResult result = service.hardDelete(input, 'profile_key', 'sa');

How to attach files to records saved to the InCountry platform

Please consider that the Apex SDK will perform an additional SOQL query to fetch a record from the Salesforce database. The record to which you want to attach the file must be saved on the InCountry platform. Using the last parameter of the method you can specify whether to delete the file from the Salesforce database after writing it to the InCountry platform.

InCountry service = InCountry.getInstance();
InCountry.FileData result = service.attachFile('003S000001XcKebIAF', 'test.txt', Blob.valueOf('test data'), false);

The FileData class contains data returned from the InCountry platform:

@JsonAccess(serializable='always' deserializable='always')
global class FileData {
@AuraEnabled global String file_id;
@AuraEnabled global String filename;
@AuraEnabled global String hash;
@AuraEnabled global String mime_type;
@AuraEnabled global Integer size;
@AuraEnabled global String created_at;
@AuraEnabled global String updated_at;
@AuraEnabled global String download_link;
@AuraEnabled global String error;
}

How to remove files from the InCountry platform

Please consider that the Apex SDK will perform additional SOQL queries to fetch your records from the Salesforce database. The record to which you want to delete the file must be saved on the InCountry platform. The last parameter is the file’s id on the InCountry platform.

InCountry service = InCountry.getInstance();
List<InCountry.DeleteResult> result = service.deleteFile('003S000001XcKebIAF','9097d392-84de-4227-9e76-fc05403747e9');

The DeleteResult class contains data returned from the InCountry platform:

global class DeleteResult {
@AuraEnabled global String key;
@AuraEnabled global Boolean isDeleted;
@AuraEnabled global String error;

global DeleteResult(String key) {
this.key = key;
}
}

How to set and reset time to live (TTL) for records with protected data

Warning

These operations are not available in the restriction data regulation model. Please consider this while using the Apex SDK.

The InCountry platform provides the ‘Recycle Bin’ feature which allows you to define the time to live for specific records and automatically delete these records with all their protected data upon reaching the specified date and time.

Setting TTL for records

Please consider that the Apex SDK will perform an additional SOQL query to fetch a record from the Salesforce database. A record will be automatically deleted. To configure how many days the records will be stored in Recycle Bin, you need to populate the InCountryValue custom metadata types.

Set TTL for single Salesforce Id

This method will perform a callout to update the record on the InCountry side.

InCountry service = InCountry.getInstance();
Id recordId = '0012100000vUyEoAAK';
service.setTTL(recordId);

Set TTL for list of Salesforce Ids

This method will automatically split the records by countries. Also, that means the method will perform a callout per a country to update the records on the InCountry side.

InCountry service = InCountry.getInstance();
Set<Id> recordIds = new Set<Id>{'0012100000vUyEoAAK'};
service.setTTL(recordIds);

Resetting TTL for records

This example shows how to reset the TTL (expires_at) value for records with protected data. The reset operation sets the expires_at value to null, which means that a record will not be automatically deleted from the InCountry platform when reaching the previously specified date (TTL).

Reset TTL for single Salesforce Id

This method will perform a callout to update the record on the InCountry side.

InCountry service = InCountry.getInstance();
Id recordId = '0012100000vUyEoAAK';
service.resetTTL(recordId);

Reset TTL for list of Salesforce Ids

This method will automatically split the records by countries. Also, that means the method will perform a callout per a country to update the records on the InCountry side.

InCountry service = InCountry.getInstance();
Set<Id> recordIds = new Set<Id>{'0012100000vUyEoAAK'};
service.resetTTL(recordIds);

How to check if a record will be processed by the InCountry platform

You can check if a record will be processed by the InCountry platform or not. There are two different methods that allow you to check either a single record or multiple records.

How to check a single record

note

This operation is available in the three-model package for the restriction and redaction models only. Please consider this while using the Apex SDK.

InCountry service = InCountry.getInstance();
Boolean result = service.isRegulatedRecord('00Q5w00001vrzjQEAQ');

How to check multiple records

note

This operation is available in the legacy package and in the three-model package for the restriction and redaction models. Please consider this while using the Apex SDK.

Please consider that the Apex SDK will not perform an SOQL query to fetch a Salesforce database record. Please ensure that all the required fields are included into your SOQL queries. The Apex SDK requires all the regulated fields and the Source Country Field field to use the record-level policy for determining the record’s country of origin.

InCountry service = InCountry.getInstance();
List<Contact> recordsToCheck = [SELECT Id, MailingCountry FROM Contact LIMIT 5];
Incountry.RegulatedRecordsResult result = service.identifyRegulatedRecords(recordsToCheck);

The RegulatedRecordsResult class contains two lists. The first list regulatedObjects contains a list of regulated records, the second list nonRegulatedObjects contains a list of non-regulated records.

@JsonAccess(serializable='always' deserializable='always')
global class RegulatedRecordsResult {
@AuraEnabled global List<SObject> regulatedObjects = new List<SObject>();;
@AuraEnabled global List<SObject> nonRegulatedObjects = new List<SObject>();
}

How to redact records with the tools of the InCountry platform

You can redact records by using two different approaches:

  1. by records instances;

  2. by records ids.

Redacting by record instances

If you have queried all the necessary records from the Salesforce database, you can provide the record instances to the redactRecords() method so that the SDK will skip additional SOQL queries. Please ensure that all the required fields are included in your SOQL queries. The Apex SDK requires all the regulated fields and the Source Country Field field to use the record-level policy for determining the record originating country.

InCountry service = InCountry.getInstance();
List<Contact> recordsToRedact = [SELECT Id, Phone, Title FROM Contact LIMIT 5];
List<SObject> redactedRecords = service.redactRecords(recordsToRedact);

The redactedRecords list will contain records from the recordsToRedact list but the values of each record will be redacted according to the InCountry regulation policies in Settings.

Redacting by records ids

In this scenario, you should provide a set of record ids. Please consider that the Apex SDK will perform additional SOQL queries to retrieve records from the Salesforce database. For example, if you provide three account Ids and five lead Ids, the SDK will perform two additional SOQL queries to retrieve accounts and leads from the database. These SOQL queries are required because the SDK should fetch all the regulated field values and the record originating countries from the database. In the following example, the SDK will fetch two leads and one account from the InCountry platform.

InCountry service = InCountry.getInstance();
Set<Id> input = new Set<Id>{'00Q5w00001vsLjVEAU', '00Q5w00001vrzjQEAQ', '0015w00002OUr9uAAD'};
List<SObject> redactedRecords = service.redactRecords(input);

How to delete all records history

InCountry service = InCountry.getInstance();
Set<Id> input = new Set<Id>{'00Q5w00001vsLjVEAU', '00Q5w00001vrzjQEAQ'};
List<DeleteHistoryResult> result = service.deleteRecordsHistory(input);

If you want to delete all history records except the “Created“ record immediately after the records sync is completed, follow this approach:

String country = 'sa';
String recordId = '0010900002NSduvAAD';

String endpoint;
for (testincountry1__InCountryRestApiEndpoint__mdt endpointItem : testincountry1__InCountryRestApiEndpoint__mdt.getAll().values()) {
if (country == endpointItem.testincountry1__Country__c) {
endpoint = endpointItem.testincountry1__Endpoint__c;
break;
}
}
String credentialApiName;
for (NamedCredential credential : [SELECT DeveloperName, Endpoint FROM NamedCredential]) {
if (credential.Endpoint == endpoint) {
credentialApiName = credential.DeveloperName;
break;
}
}

String findHistoryRequestBody = '{'
+' "country":"'+country+'",'
+' "filter":{'
+' "parent_key":"'+recordId+'"'
+' },'
+' "options":'
+' {'
+' "fields":["record_key"],'
+' "offset":1,'
+' "limit":100,'
+' "sort":[{"updated_at":"asc"}]'
+' }'
+'}';
HttpRequest findRequest = new HttpRequest();
findRequest.setMethod('POST');
findRequest.setBody(findHistoryRequestBody);
findRequest.setHeader('Content-Type', 'application/json');
findRequest.setEndpoint('callout:' + credentialApiName + '/api/records/find');

// send request to find history record_keys in InCountry
HttpResponse response = (new Http()).send(findRequest);

Map<String, Object> responseWrapper = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());
List<Object> recordKeyWrappers = (List<Object>)responseWrapper.get('data');

Set<String> historyProfileKeysToDelete = new Set<String>();
for (Object recordKeyWrapperItem : recordKeyWrappers) {
Map<String, Object> recordKeyWrapper = (Map<String, Object>)recordKeyWrapperItem;
historyProfileKeysToDelete.add((String)recordKeyWrapper.get('record_key'));
}

String historyDeleteRequest = '{"filter": {"record_key":' +JSON.serializePretty(historyProfileKeysToDelete)+ '}}';

HttpRequest deleteRequest = new HttpRequest();
deleteRequest.setMethod('POST');
deleteRequest.setBody(historyDeleteRequest);
deleteRequest.setHeader('Content-Type', 'application/json');
deleteRequest.setEndpoint('callout:' + credentialApiName + '/api/records/batch/delete');

// send request to delete all history records except "Created."
HttpResponse deleteResponse = (new Http()).send(deleteRequest);
// STATUS 204 - success

How to redact a value in a Salesforce object with the tools of the InCountry platform

InCountry service = InCountry.getInstance();
List<Contact> recordsToRedact = [SELECT Id, Phone, Title FROM Contact LIMIT 1];
SObject redactedRecord = service.redactField(recordsToRedact[0], 'Title');

The redactedRecord list will contain a record but the record value will be redacted according to the InCountry regulation policies in Settings.

How to redact a value with the tools of the InCountry platform

InCountry service = InCountry.getInstance();
String valueToRedact = 'John Martin';
String redactionAlgorith = 'sha256';
Object redactedValue = service.redactValue(valueToRedact, redactionAlgorith);

The redactedValue variable is redacted according to the InCountry redaction rules.

Supported algorithms

The InCountry Apex SDK supports the following redactionAlgorithms types:

  • uniqueHash

  • uniqueEmailHash

  • sha256

  • dtkSha256

  • defaultNumber

  • defaultBoolean

  • defaultDate

  • defaultDateTime

  • nothing

  • defaultText

  • valueOne

  • fixed

Also, values can be redacted with the formula redaction algorithm, but with the following code:

InCountry service = InCountry.getInstance();
String valueToRedact = 'John Martin';
String redactionAlgorithm = 'sha256';
String formula = '{
"fn":"dtkSha256",
"transform":["trim"],
"format":"{18}"
}';
Map<String, Object> redactionParams = new Map<String, Object> {
'formula' => formula
};
Object redactedValue = service.redactValue(valueToRedact, redactionAlgorithm, redactionParams);

The formula redaction algorithm requires the additional parameter formula.

note

For the details, please check our documentation for the formula function.

note

Hash functions work only with values, if there are no values, the hash function is not applied.

Execute business logic after record create/update on InCountry Record Detail

To execute business logic after the record creation or update, you can use a conventional Apex Trigger or Flow with the Apex SDK. However, these methods may appear unreliable if regulated data is subject to processing after the record creation or update. The reason for that is that regulated values are synchronized with the InCountry platform only after execution of a DML operation. Consequently, when retrieving regulated values from the InCountry platform with the Apex SDK, the values of the requested record cannot be fetched as they have not been synchronized yet.

To bypass this issue, you can use resident functions that are executed immediately once the record has been synchronized with the InCountry platform. They enable the sending of a REST API request to Salesforce to execute an Apex REST API request, which in turn will execute the necessary post-create/update business logic.

In the following example, the resident function calls Apex REST API:

module.exports.handler = async function(storage, country, params, modules) {
const recordId = params.id;
const sfdcBaseUrl = params.sfdcBaseUrl;
const token = params.token;

const DEFAULT_HEADERS = {
headers: {
"Authorization": "Bearer " + token,
"Content-Type": "application/json"
}
};
const endpoint = sfdcBaseUrl + '/services/apexrest/businessProcess';
await modules.axios.post(
endpoint, {
recordId: recordId
},
DEFAULT_HEADERS
);
}

This example shows an Apex class that is exposed as web service:

@RestResource(urlMapping='/businessProcess')
global with sharing class BusinessProcessRestService {

@HttpPost
global static void businessProcess(String recordId) {
testInCountry1.InCountry service = testInCountry1.InCountry.getInstance();
List<testInCountry1.InCountry.FindResult> result = service.find(new Set<Id>{recordId});
system.debug(result[0]);
// the line where regulated data can be processed
}
}

Check if records are synchronized with InCountry

Please consider that the Apex SDK will perform an additional SOQL query to fetch a record from the Salesforce database. The code below checks if the records are synchronized with InCountry and returns the response with two fields: syncIds and nonSyncIds where the ids of the synced and non-synced records will be stored.

InCountry service = InCountry.getInstance();
Set<Id> recordsIdSet = new Set<Id>{'0038F00000RAiWnQAL','0038F00000RBuCYQA1'};
CheckSyncRecordsResult result = service.checkSyncRecords(recordsIdSet);

Error handling

The Apex SDK may throw the following exception:

  • SObject was retrieved without a required field. For example, you need to specify the Source Country field if you are using a record-level policy set for a current SObject.

    Execute Anonymous Error

Apex SDK Unit Testing

When you insert or update records in Salesforce through the Salesforce REST API without using the user interface, regulated data will appear directly in the Salesforce database. To synchronize regulated data to the InCountry platform, you need to use a trigger. As an example let’s take a Lead object.

trigger LeadTrigger on Lead (after insert, after update) {
InCountrySyncHandler.executeSyncFromTrigger();
}
public class InCountrySyncHandler implements Queueable, Database.AllowsCallouts {

public static Boolean isDisabled = false;

private List<SObject> objectsToSync;

public InCountrySyncHandler(List<SObject> objectsToSync) {
this.objectsToSync = objectsToSync;
}

public void execute(QueueableContext context) {
if (this.objectsToSync != null && !this.objectsToSync.isEmpty()) {
testInCountry1.InCountry incountryApexSDK = testInCountry1.InCountry.getInstance();
List<SObject> clonedRecordsToUpdate = new List<SObject>();
for (SObject recordItem : this.objectsToSync) {
clonedRecordsToUpdate.add(recordItem.clone(true, true));
}
InCountrySyncHandler.isDisabled = true; // prevent queueable running on update
List<testInCountry1.InCountry.WriteRequest> syncResults = incountryApexSDK.sync(clonedRecordsToUpdate);
System.debug(LoggingLevel.ERROR, '\\n\\n --- InCountrySyncHandler - execute - 2 ---'
+'\\n - syncResults: ' + syncResults
+'\\n');
}
}

public static Id executeSyncFromTrigger() {
if (isDisabled) return null;
List<Lead> leadsToSync = new List<Lead>();
if (Trigger.isAfter && (Trigger.isInsert || Trigger.isUpdate)) {
for (Lead leadItem : (List<Lead>)Trigger.new) {
if (leadItem.LeadSource == 'API') {
leadsToSync.add(leadItem);
}
}
}

return executeSync(leadsToSync);
}

public static Id executeSync(List<SObject> objectsToProcess) {
if (isDisabled) return null;
if (objectsToProcess == null || objectsToProcess.isEmpty()) return null;

testInCountry1.InCountry incountryApexSDK = testInCountry1.InCountry.getInstance();
testInCountry1.InCountry.RegulatedRecordsResult regulatedResult = incountryApexSDK.identifyRegulatedRecords(objectsToProcess);
System.debug(LoggingLevel.ERROR, '\\n\\n --- InCountrySyncHandler - executeSync - 2 ---'
+'\\n - regulatedResult: ' + regulatedResult
+'\\n');

if (!regulatedResult.regulatedObjects.isEmpty()) {
return System.enqueueJob(new InCountrySyncHandler(regulatedResult.regulatedObjects));
}
return null;
}

}

The Unit Test classes are as follows:

@isTest
private class InCountrySyncHandlerTest {

static final String HTTP_POST_METHOD = 'POST';
static final String HTTP_DELETE_METHOD = 'DELETE';
static final String COUNTRY_OF_RESIDENCY = 'sa';

static final String REGULATED_COUNTRY_FIELD_VALUE = 'Saudi Arabia';
static final String LEAD_SOURCE_FIELD_VALUE = 'API';

@TestSetup
static void testSetup() {
insert new testIncountry1__Object_relationship__c(
testIncountry1__Behavior__c = 'restriction',
testIncountry1__CountryFieldValue__c = REGULATED_COUNTRY_FIELD_VALUE,
testIncountry1__Country__c = 'sa',
testIncountry1__Country_field__c = 'Country',
testIncountry1__Object_name__c = 'Lead',
testIncountry1__Type__c = 'record',
testIncountry1__Criteria__c = '{"operator":"==","fieldName":"Country","value":"'+REGULATED_COUNTRY_FIELD_VALUE+'"}'
);
insert new List<testIncountry1__Object_relationship_fields__c> {
new testIncountry1__Object_relationship_fields__c(
testIncountry1__Field_name__c = 'LastName',
testIncountry1__HashFunction__c = 'uniqueHash',
testIncountry1__Object_name__c = 'Lead'
),
new testIncountry1__Object_relationship_fields__c(
testIncountry1__Field_name__c = 'Email',
testIncountry1__HashFunction__c = 'uniqueEmailHash',
testIncountry1__Object_name__c = 'Lead'
)
};
}

@isTest
static void executeSync() {
String email = 'john.dou' + Datetime.now().getTime() + '@mail.sa';
String lastName = 'Dou' + Datetime.now().getTime();

Lead lead1 = new Lead(
Email = email,
LastName = lastName,
Company = 'Unknown',
Country = REGULATED_COUNTRY_FIELD_VALUE,
LeadSource = LEAD_SOURCE_FIELD_VALUE
);
InCountrySyncHandler.isDisabled = true;
insert lead1;

testInCountry1.InCountryHttpCalloutSimulator.setMock(new InCountryRestAPIMock(new Lead(
Id = lead1.Id,
LastName = lead1.LastName,
Email = lead1.Email
)));
Test.startTest();

InCountrySyncHandler.isDisabled = false;
Id queueableId = InCountrySyncHandler.executeSync(new List<SObject>{lead1});

Test.stopTest();

Assert.isNotNull(queueableId);
lead1 = [SELECT Email FROM Lead WHERE Id = :lead1.Id];
Assert.areNotEqual(email, lead1.Email);
}

@isTest
static void executeSync_nothingToSync() {
String email = 'john.dou' + Datetime.now().getTime() + '@mail.sa';
String lastName = 'Dou' + Datetime.now().getTime();

Lead lead1 = new Lead(
Email = email,
LastName = lastName,
Company = 'Unknown',
Country = '',
LeadSource = LEAD_SOURCE_FIELD_VALUE
);
InCountrySyncHandler.isDisabled = true;
insert lead1;

testInCountry1.InCountryHttpCalloutSimulator.setMock(new InCountryRestAPIMock(new Lead(
Id = lead1.Id,
LastName = lead1.LastName,
Email = lead1.Email
)));
Test.startTest();

InCountrySyncHandler.isDisabled = false;
Id queueableId = InCountrySyncHandler.executeSync(new List<SObject>{lead1});

Test.stopTest();

Assert.isNull(queueableId);
lead1 = [SELECT Email FROM Lead WHERE Id = :lead1.Id];
Assert.areEqual(email, lead1.Email);
}

@isTest
static void executeSyncFromTrigger() {
String email = 'john.dou' + Datetime.now().getTime() + '@mail.sa';
String lastName = 'Dou' + Datetime.now().getTime();

testInCountry1.InCountryHttpCalloutSimulator.setMock(new InCountryRestAPIMock(new Lead(
Id = Lead.SObjectType.getDescribe().getKeyPrefix() + '000000000001',
LastName = lastName,
Email = email
)));
Test.startTest();

Lead lead1 = new Lead(
Email = email,
LastName = lastName,
Company = 'Unknown',
Country = REGULATED_COUNTRY_FIELD_VALUE,
LeadSource = LEAD_SOURCE_FIELD_VALUE
);
insert lead1;

Test.stopTest();

lead1 = [SELECT Email FROM Lead WHERE Id = :lead1.Id];
Assert.areNotEqual(email, lead1.Email);
}

private class InCountryRestAPIMock implements HttpCalloutMock {

private String recordId;
private String recordJsonBody;

public InCountryRestAPIMock(SObject record) {
this.recordId = record.Id;
Map<String, Object> recordMap = (Map<String, Object>) JSON.deserializeUntyped(JSON.serialize(record));
if (recordMap.containsKey('attributes')) {
recordMap.remove('attributes');
}
this.recordJsonBody = JSON.serialize(recordMap);
System.debug(LoggingLevel.ERROR, '\\n\\n --- LeadTriggerTest - InCountryRestAPIMock ---'
+'\\n - this.recordJsonBody: ' + this.recordJsonBody
+'\\n');
}

public HttpResponse respond(HttpRequest req) {
HttpResponse response = new HttpResponse();
String endpoint = req.getEndpoint();
String requestMethod = req.getMethod();
if (endpoint.contains('/api/records/find')) {
String findResponseBody = '{ "data":[ { "country":"sa", "key":"' + this.recordId +'", "record_key":"' + this.recordId +'", "profile_key":"' + this.recordId +'",'
//+' "body":"{\\\\"LastName\\\\":\\\\"Dou'+Datetime.now().getTime()+'\\\\",\\\\"Length_of__c\\\\":2,\\\\"Apttus__Status_Category__c\\\\":\\\\"Activated\\\\",\\\\"Apttus__Status__c\\\\":\\\\"In Effect\\\\"}"'
+ ' "body":"' + this.recordJsonBody + '"'
+', "created_at":"2021-02-14T12:56:15.000Z", "updated_at":"2021-02-14T12:56:15.000Z", "version":0 } ], "meta":{ "count":1, "limit":3, "offset":0, "total":1 } }';
System.debug(LoggingLevel.ERROR, '\\n\\n --- LeadTriggerTest - InCountryRestAPIMock - respond ---'
+'\\n - endpoint: ' + endpoint
+'\\n - findResponseBody: ' + findResponseBody
+'\\n');
response.setBody(findResponseBody);
response.setStatusCode(200);
} else if (endpoint.contains('/files') && requestMethod == HTTP_POST_METHOD) {
testIncountry1.InCountry.FileData fileData = new testIncountry1.InCountry.FileData();
response.setBody(JSON.serialize(fileData));
response.setStatusCode(201);
} else if (endpoint.contains('/files') && requestMethod == HTTP_DELETE_METHOD) {
response.setBody(req.getBody());
response.setStatusCode(204);
} else if (!endpoint.contains('/batch') && requestMethod != HTTP_DELETE_METHOD) {
response.setBody(req.getBody());
response.setStatusCode(200);
} else if (endpoint.contains('/api/records/batch')) {
testIncountry1.InCountry.WriteRequest wr1 = new testIncountry1.InCountry.WriteRequest(
COUNTRY_OF_RESIDENCY,
this.recordId,
this.recordId,
this.recordJsonBody
);
testIncountry1.InCountry.BatchWriteRequest batchRequest = new testIncountry1.InCountry.BatchWriteRequest(
'sa',
new List<testIncountry1.InCountry.WriteRequest> {wr1}
);
response.setBody(JSON.serialize(batchRequest.records));
System.debug(LoggingLevel.ERROR, '\\n\\n --- LeadTriggerTest - InCountryRestAPIMock - respond ---'
+'\\n - endpoint: ' + endpoint
+'\\n - response.getBody(): ' + response.getBody()
+'\\n');
if (endpoint.contains('/api/records/batch/update')) {
response.setStatusCode(200);
} else {
response.setStatusCode(201);
}
}
return response;
}
}
}
note

When performing unit tests, you can use only the hardcoded sa country: testIncountry1__Country__c = ‘sa'