Apex Coding Interview Challenge #10

Given a string of round, curly, and square open and closing brackets, return whether the brackets are balanced (well-formed).

For example, given the string “([])[]({})”, you should return true.

Given the string “([)]” or “((()”, you should return false.

There is are Stack implementation currently available in Apex so we can use a List to push and pop characters

Solution

public class Stack {
    private List<Object> items {get; set;}
    
    public Stack() {
        this.items = new List<Object>();
    }
    
    public Integer size() {
        return this.items.size();
    }

    public Boolean isEmpty() {
        return size() == 0;
    }
        
    public void push(Object itemToPush) {
        this.items.add(itemToPush);
    }
    
    public Object pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        
        return this.items.remove(size() - 1);
    }
    
    public Object peek() {
        if (isEmpty()) {
            throw new EmptyStackException('Stack is empty');
        }
        
        return this.items.get(size() - 1);
    }    
}

public class EmptyStackException extends Exception {}

public static Map<String, String> mapBrackets(){
    Map<String, String> bracketMap = new Map<String, String>();
    bracketMap.put('}', '{');
    bracketMap.put(')', '(');
    bracketMap.put(']', '[');
    return bracketMap;
}

public static Boolean checkClosingBrackets(String bracketStr) {
    Map<String, String> bracketMap = mapBrackets();
    Stack customStack = new Stack();
    
    for (Integer i=0; i < bracketStr.length(); i++){
        String str = bracketStr.substring(i,i+1);
        System.debug('> ' + str);
        if (!bracketMap.containsKey(str)){
            customStack.push((String)str);
        } else {
            String pop = (String)customStack.pop();
            String mappedVal = bracketMap.get(str);
            
            if (!mappedVal.equals(pop)){
                return false;
            }
        }
    }
    
    if (!customStack.isEmpty()){
        return false;
    }
    
    return true;
}

Testing

System.debug(checkClosingBrackets('([])[]({})')); //true
System.debug(checkClosingBrackets('([])[]({}'));  //false

Apex Coding Interview Challenge #9

Given a list of integers, write a function that returns the largest sum of non-adjacent numbers. Numbers can be 0 or negative.

For example, [2, 4, 6, 2, 5] should return 13, since we pick 2, 6, and 5. [5, 1, 1, 5] should return 10, since we pick 5 and 5.

Follow-up: Can you do this in O(N) time and constant space?

Solution

public Integer sumLargestNonAdjacentNumbers(List<Integer> lstNumbers){
      Integer exclusive = 0;
      Integer inclusive = lstNumbers.get(0);
      for (Integer i = 1; i < lstNumbers.size(); i++) {
        Integer temp = inclusive;
        inclusive = Math.max(exclusive + lstNumbers.get(i), inclusive);
        exclusive = temp;
      }
      return inclusive;
}

Testing

System.debug(sumLargestNonAdjacentNumbers(new List<Integer>{5, 1, 1, 5})); //10

System.debug(sumLargestNonAdjacentNumbers(new List<Integer>{2, 4, 6, 2, 5})); //13

Complexity Analysis
Time complexity: O(n)
Space complexity: O(1)

Apex Coding Interview Challenge #8

Implement an autocomplete system. That is, given a query string s and a set of all possible query strings, return all strings in the set that have s as a prefix.

For example, given the query string de and the set of strings [dog, deer, deal], return [deer, deal].

Hint: Try preprocessing the dictionary into a more efficient data structure to speed up queries.

Solution

public Map<String, List<String>> mapOfDictionary(List<String> wordsLst){
    Map<String, List<String>> dictionaryMap = new Map<String, List<String>>();
    
    for (String w : wordsLst){
        List<String> wordChars = new List<String>(w.split(''));
        String concatChars = '';
        for (String wordChar : wordChars){
            concatChars += wordChar;
            if (dictionaryMap.containsKey(concatChars)){
                List<String> wordsForChars = dictionaryMap.get(concatChars);
                wordsForChars.add(w);
                dictionaryMap.put(concatChars, wordsForChars);
            } else {
                dictionaryMap.put(concatChars, new List<String>{w});
            }
        }   
    }
   
    return dictionaryMap;
}

public List<String> getWordsForAutoComplete(String word){
    Map<String, List<String>> dictionaryMap = mapOfDictionary(new List<String>{'dog', 'deer', 'deal'});
    return dictionaryMap.get(word);
}

Testing

System.debug(getWordsForAutoComplete('de')); //(deer, deal)

Complexity Analysis
Time complexity: O(n^2)
Space complexity:  O(m*n) //m – max length of string, n – amount of words

Apex Coding Interview Challenge #7

Given an array of integers, find the first missing positive integer in linear time and constant space. In other words, find the lowest positive integer that does not exist in the array. The array can contain duplicates and negative numbers as well.

For example, the input [3, 4, -1, 1] should give 2. The input [1, 2, 0] should give 3.

Solution

public Integer lowestIntMissingFromLst(List<Integer> intVals){
  Integer missingNum = 0;
  Integer maxVal = 0;
  Integer maxMin = 0;
  Integer maxMax = 0;
  for (Integer k = 0; k < intVals.size(); k++){
      if (k == 0){
          maxVal = intVals.get(k);
      } else if (maxVal < intVals.get(k)){
          maxVal =  intVals.get(k);
          maxMin = intVals.get(k) - intVals.size();
          maxMax = intVals.get(k) + intVals.size();
      }
      
      Integer plusOne = intVals.get(k)+1;
      Integer minusOne = intVals.get(k)-1;
      
      if (plusOne < maxMax && !intVals.contains(plusOne)){
          missingNum = plusOne;
      } else if (minusOne < maxMin && !intVals.contains(minusOne)){
          missingNum = minusOne;
      }
  }
  return missingNum;
}

Test

System.debug(lowestIntMissingFromLst(new List<Integer>{3, 4, -1, 1})); //2

System.debug(lowestIntMissingFromLst(new List<Integer>{1, 2, 0})); //3

Complexity Analysis
Time complexity: O(n)
Space complexity: O(1)

Apex Coding Interview Challenge #6

Given an array of integers, return a new array such that each element at index i of the new array is the product of all the numbers in the original array except the one at i.

For example, if our input was [1, 2, 3, 4, 5], the expected output would be [120, 60, 40, 30, 24]. If our input was [3, 2, 1], the expected output would be [2, 3, 6].

Solution

public List<Integer> productOfNumbers(List<Integer> intVals){
   List<Integer> productLst = new List<Integer>();
   for (Integer k = 0; k < intVals.size(); k++){
     Integer sumOfVals = 1; // it's 1 because multiplying by 0 will always be 0
     for (Integer j = 0; j < intVals.size(); j++){
      if (j != k){
        sumOfVals *= intVals.get(j);
      }
     }
    productLst.add(sumOfVals);
   }
  return productLst;
}

Test

System.debug(productOfNumbers(new List<Integer>{1, 2, 3, 4, 5})); //(120, 60, 40, 30, 24)

System.debug(productOfNumbers(new List<Integer>{3, 2, 1})); //(2, 3, 6)

Complexity Analysis
Time complexity: O(n2)
Space complexity: O(n)

Salesforce Platform Events, Change Data Capture, Real-Time Event Monitoring explained

Below details the different Salesforce events types for different use cases

1. Change Data Capture

Select the entities that generate change event notifications. Change Data Capture sends notifications for created, updated, deleted, and undeleted records. All custom objects and a subset of standard objects are supported.

/data/AccountChangeEvent – any changes to accounts

  • Create account
 INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"LastModifiedDate":"2020-05-11T19:32:35.000Z","CleanStatus":"Pending","OwnerId":"0054P000009tuVoQAI","CreatedById":"0054P000009tuVoQAI","Match_Billing_Address__c":false,"ChangeEventHeader":{"commitNumber":10834564859700,"commitUser":"0054P000009tuVoQAI","sequenceNumber":1,"entityName":"Account","changeType":"CREATE","changedFields":[],"changeOrigin":"","transactionKey":"00031938-a4a4-1f2e-d772-c2527e20cff6","commitTimestamp":1589225555000,"recordIds":["0014P00002lJ0ltQAC"]},"CreatedDate":"2020-05-11T19:32:35.000Z","LastModifiedById":"0054P000009tuVoQAI","Name":"Account Event","Total_Assets__c":0.0}
  • Update account name
c.g.g.s.s.SalesforcePlatformEventHandler - {"LastModifiedDate":"2020-05-11T19:33:42.000Z","ChangeEventHeader":{"commitNumber":10834565354322,"commitUser":"0054P000009tuVoQAI","sequenceNumber":1,"entityName":"Account","changeType":"UPDATE","changedFields":["Name","LastModifiedDate"],"changeOrigin":"","transactionKey":"000320f7-ff66-5c6d-e66d-540dc1dba750","commitTimestamp":1589225622000,"recordIds":["0014P00002lJ0ltQAC"]},"Name":"Account Event123"}
  • Delete account
INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"ChangeEventHeader":{"commitNumber":10834566118523,"commitUser":"0054P000009tuVoQAI","sequenceNumber":1,"entityName":"Account","changeType":"DELETE","changedFields":[],"changeOrigin":"","transactionKey":"000273af-397f-c5ce-b95a-412eb6071ee2","commitTimestamp":1589225728000,"recordIds":["0014P00002lJ0ltQAC"]}}

2. Real-Time Event Monitoring

Real-Time Event Monitoring helps you monitor and detect standard events in Salesforce in near real-time. You can store the event data for auditing or reporting purposes.

/event/LoginEventStream – LoginEventStream tracks login activity of users who log in to Salesforce. This object is available in API version 46.0 and later.

c.g.g.s.s.SalesforcePlatformEventHandler - {"EventDate":"2020-05-11T22:25:59.000Z","CountryIso":"US","Platform":"Unknown","EvaluationTime":0.0,"CipherSuite":"ECDHE-RSA-AES256-GCM-SHA384","PostalCode":"20149","ClientVersion":"N/A","LoginGeoId":"04F4P00008ybrGk","LoginUrl":"login.salesforce.com","LoginHistoryId":"0Ya4P0000DF17laSQB","CreatedById":"0054P000009y5URQAY","ApiType":"N/A","LoginType":"Remote Access 2.0","Status":"Success","AdditionalInfo":"{}","ApiVersion":"N/A","EventIdentifier":"46f6deba-e3de-4b07-b294-3ed56736da36","LoginLatitude":39.0481,"City":"Ashburn","Subdivision":"Virginia","NetworkId":"000000000000000","SourceIp":"34.230.66.105","Username":"blah@gmail.com","UserId":"0054P000009tuVoQAI","CreatedDate":"2020-05-11T22:26:05.711Z","Country":"United States","LoginLongitude":-77.4728,"TlsProtocol":"TLS 1.2","LoginKey":"LlUdqQsfSrgiRTDY","Application":"Event App","UserType":"Standard","HttpMethod":"POST","SessionLevel":"STANDARD","Browser":"Unknown"}

/event/ApiEventStream – Tracks these user-initiated read-only API calls: query(), queryMore(), and count()

[HttpClient@771a660-23] INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"EventDate":"2020-05-11T22:28:34.000Z","Platform":"iOS/Mac","Query":"select AccountNumber, AccountSource, Active__c, AnnualRevenue, BillingAddress, BillingGeocodeAccuracy, ChannelProgramLevelName, ChannelProgramName, CleanStatus, CreatedById, CreatedDate, CustomerPriority__c, DandbCompanyId, Description, DunsNumber, Fax, Id, Industry, IsCustomerPortal, IsDeleted, IsPartner, Jigsaw, JigsawCompanyId, LastActivityDate, LastModifiedById, LastModifiedDate, LastReferencedDate, LastViewedDate, MasterRecordId, Match_Billing_Address__c, NaicsCode, NaicsDesc, Name, Number_of_Contacts__c, NumberOfEmployees, NumberofLocations__c, OwnerId, Ownership, Ownership_Check__c, ParentId, Phone, PhotoUrl, Rating, ShippingAddress, ShippingGeocodeAccuracy, Sic, SicDesc, Site, SLA__c, SLAExpirationDate__c, SLASerialNumber__c, SystemModstamp, TickerSymbol, Total_Assets__c, Tradestyle, Type, UpsellOpportunity__c, Website, YearStarted from Account limit 1","EvaluationTime":0.0,"ElapsedTime":51,"Operation":"Query","LoginHistoryId":"0Ya4P0000DF17dtSQB","CreatedById":"0054P000009y5URQAY","SessionKey":"do1y6L0Hc/MNQPEU","ApiType":"SOAP Partner","UserAgent":"SoqlXplorer/3.3 CFNetwork/902.6 Darwin/17.7.0 (x86_64)","Client":"SoqlXplorer/3.3","Records":"{\"totalSize\":1,\"done\":true,\"records\":[{\"attributes\":{\"type\":\"Account\"},\"Id\":\"0014P00002gqPpcQAE\"}]}","AdditionalInfo":"{}","ApiVersion":47.0,"EventIdentifier":"f91c687b-91ab-4c11-8235-d4db95eeb7cc","RowsProcessed":1.0,"SourceIp":"69.181.110.193","Username":"thysmichels@gmail.com","UserId":"0054P000009tuVoQAI","CreatedDate":"2020-05-11T22:28:38.217Z","LoginKey":"PsOEa5SkbjVf4QBC","Application":"N/A","QueriedEntities":"Account","SessionLevel":"STANDARD"}

3. Custom Platform Events

/event/ESBBus__e – custom event bus messages published via Apex: EventBus.publish()

[HttpClient@51c668e3-20] INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"Name__c":"Test","CreatedById":"0054P000009tuVoQAI","CreatedDate":"2020-05-12T02:39:33.955Z"}

Apex Rest API Architecture – Best Practices

What’s a Service/API

A service/API is a function that is well-defined, self-contained, and does not depend on the context or state of other services.

1. Abstract vs Concrete

When developing software we often use abstraction and polymorphism to get most of our applications. We want to reuse as much of the code as possible. Should we write our APIs that way too?The answer is NO. Concrete is better than abstract.

Can you guess why?Let me show you a few examples.

Let’s look at two API versions. Is it better to have an API that has one /customers or an API that has /accounts/contacts and, /users separately?Which one seems more descriptive to you as a developer? Which API would you rather use?I would always choose the second one.

2. URI Formatting

Treat the resource like a noun, and the HTTP method as a verb. If you do it like that, you’ll end up with something like this:

RESOURCE POST GET PUT DELETE
/accounts Creates a new account List accounts Replace accounts with new accounts (Bulk update) Delete all accounts
/accounts/0014P00002gqPpm Return error Show specific account If exist update account else error Delete specific account

This is a cleaner and more precise way to use the API. It is immediately clear to the end user, and there is a method to the madness.

Use a constants class to define all the endpoints and use {version} so it becomes a variable for each endpoint. So we can manage different versions of and endpoint in the same class.

public with sharing class Rest_Constants {

    public static final String API_VERSION = '{version}';
    public static final String API_VERSION_STR = '/' + API_VERSION;

    public static final String URI_ACCOUNTS =  API_VERSION_STR + '/accounts';
}

Return all endpoint declarations as part of /endpoints declaration
https://na132.salesforce.com/services/apexrest/v1/settings/endpoints

{
    "result": [
        {
            "path": {
                "operation": "HTTPGET",
                "uri": "/{version}/accounts"
            }
        }
    ],
    "timeStamp": "2020-05-06T16:49:00.671Z",
    "totalTime": 0.037,
    "userId": "0054P00000C8OBlQAN"
}

I prefer using plurals for resources names, read more here: https://stackoverflow.com/questions/6845772/rest-uri-convention-singular-or-plural-name-of-resource-while-creating-it

3. Error Handling

This is another important aspect of  API building. There are a few good ways to handle errors.
Create a static resource file with all the define errors of the app

[
	{
	 "id": "QUERY_OPERATION_FAILED",
	 "code": 991,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Query operation failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "SOBJECT_OPERATION_FAILED",
	 "code": 992,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "SObject operation failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "JSON_SERIALIZATION_FAILED",
	 "code": 993,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Json serialization failed, check if Heroku is up or restart services UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "DATABASE_OPERATION_FAILED",
	 "code": 994,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Database operation failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "CALLOUT_FAILED",
	 "code": 995,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Callout to external service failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "SEARCH_FAILED",
	 "code": 996,
	 "clientTemplate": "Searching failed, try again",
	 "systemTemplate": "Searching Salesforce failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "REST_DISPATCHER_DEBUG",
	 "code": 997,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Debug UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
		"id": "SERIALIZATION_FAILED",
		"code": 998,
		"clientTemplate": "Encountered an error",
		"systemTemplate": "Serialization failed, error: {0}"
	},
	{
		"id": "REST_DISPATCHER_FAILED",
		"code": 999,
		"clientTemplate": "Encountered an error",
		"systemTemplate": "An error occurred UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	}
]

Create a object representation of the error codes from the static resources

global with sharing class Rest_Error_ResponseCode {

  public String id { get; set; }
  public Integer code { get; set; }
  public String clientTemplate { private get; set { clientTemplate = value; } }
  public String systemTemplate { private get; set { systemTemplate = value; } }

  public String formattedClientMessage { get; set; }
  public String formattedSystemMessage { get; set; }

  global void formatMessages(List<String> args, Exception ex) {
    if (args != null && args.size() != 0) {
      formattedClientMessage = String.format(clientTemplate, args);
      if (systemTemplate != null) {
        formattedSystemMessage = String.format(systemTemplate, args);
      }
    } else {
      formattedClientMessage = clientTemplate;
      formattedSystemMessage = systemTemplate;
    }
    if (ex != null) {
      if (formattedSystemMessage == null) {
        formattedSystemMessage = formattedClientMessage;
      }
      formattedSystemMessage += ' -> Cause by: ' + ex.getTypeName().stripHtmlTags() + ' - ' + ex.getMessage().stripHtmlTags() + '. Cause trace: ' + ex.getStackTraceString().stripHtmlTags();
    }
  }

  global Rest_Error_ResponseCode copy() {
    Rest_Error_ResponseCode theNewObj = new Rest_Error_ResponseCode();
    theNewObj.id = this.id;
    theNewObj.code = this.code;
    theNewObj.clientTemplate = this.clientTemplate;
    theNewObj.systemTemplate = this.systemTemplate;
    return theNewObj;
  }

    global Rest_Error_ResponseCode(){}

  global override String toString() {
    return 'BrightPlan_ResponseCode(id=' + id + ';code=' + code + ';clientTemplate=' + clientTemplate + ';systemTemplate=' + systemTemplate + ';formattedClientMessage=' + formattedClientMessage + ';formattedSystemMessage=' + formattedSystemMessage + ')';
  }
}

Create a util to read the static resource file and create a map of error codes

public with sharing class Rest_Error_ResponseCodes_Util {

   private static Map<String, Rest_Error_ResponseCode> itsResponseCodes = new Map<String, Rest_Error_ResponseCode>();

   static {
    try {
      StaticResource theCodesSrc = [SELECT Id, Body FROM StaticResource WHERE Name = 'Rest_Error_ResponseCodes' LIMIT 1];
      if (theCodesSrc != null) {
            String theCodesJSON = theCodesSrc.Body.toString();
        List<Rest_Error_ResponseCode> theCodesList = (List<Rest_Error_ResponseCode>) JSON.deserialize(theCodesJSON, List<Rest_Error_ResponseCode>.class);
        for (Rest_Error_ResponseCode theCode: theCodesList) {
          itsResponseCodes.put(theCode.id, theCode);
        }
        System.debug('Loaded ' + itsResponseCodes.size() + ' response codes into static map');
      } else {
        System.debug(LoggingLevel.ERROR, 'Cannot query Error_ResponseCodes static resource from DB');
      }
    } catch (Exception ex) {
      System.debug(LoggingLevel.ERROR, 'ERROR loading response codes: ' + ex.getMessage());
    }

  }

  public static Rest_Error_ResponseCode getCode(String anID) {
    return getCode(anID, null, null);
  }

  public static Rest_Error_ResponseCode getCode(String anID, Exception anExp) {
    return getCode(anID, anExp, null);
  }

  public static Rest_Error_ResponseCode getCode(String anID, List<String> args) {
    return getCode(anID, null, args);
  }

  public static Rest_Error_ResponseCode getCode(String anID, Exception anExp, List<String> args) {
    Rest_Error_ResponseCode theCode = itsResponseCodes.get(anID);
    if (theCode == null) {
      theCode = itsResponseCodes.get('UNEXPECTED_RESPONSE_CODE').copy();
      List<String> theArgs = new List<String>();
      theArgs.add(anID);
      if (args != null) {
        theArgs.add(args + '');
      }
      theCode.formatMessages(theArgs, anExp);
      System.debug(LoggingLevel.ERROR, 'Unknown response code ' + anID + '. Returning UNEXPECTED_RESPONSE_CODE and ignoring args list ' + args);
    } else {
      theCode = theCode.copy();
      theCode.formatMessages(args, anExp);
    }
    return theCode;
  }
}

Errors returned will look like follows

{
    "responseCode": {
        "clientTemplate": "Incorrect operation, please try again",
        "code": 989,
        "formattedClientMessage": "Incorrect operation, please try again",
        "formattedSystemMessage": "Url does not exist or incorrect operation UserId: 0054P00000C8OBlQAN Type: HTTPGET URI: /v1/account Body: ",
        "id": "URL_INVALID",
        "systemTemplate": "Url does not exist or incorrect operation UserId: {0} Type: {1} URI: {2} Body {3}"
    },
    "timeStamp": "2020-05-07T17:21:47.278Z",
    "totalTime": 0.053,
    "userId": "0054P00000C8OBlQAN",
    "mapKeysFlag": false,
    "serializeNulls": false
}

4. API Versioning

Use the letter ‘v’ in the URL to denote the API version as below and handle the versions in the Rest_Dispatcher.Dispatchable

global with sharing class Rest_Accounts {

    global with sharing class Get implements Rest_Dispatcher.Dispatchable {
        private Rest_Request requestBody;

        global String getURIMapping(){
           return Rest_Constants.URI_ACCOUNTS;
        }

        global void setRequestBody(Rest_Request requestBody){
            this.requestBody = requestBody;
        }

        global Rest_Response execute(Map<String, String> parameters){
            Rest_Response restResponse = new Rest_Response();
            String version = parameters.get('version');
            
            if ('v1'.equals(version)){
                ...
            } else if ('v2'.equals(version)){
                ...
            } else if ('v3'.equals(version)){
                ...
            } 
            
            restResponse.setResult('');
            return restResponse;
        }

        global override String toString(){
          Rest_Endpoint.Path path = new Rest_Endpoint.Path();
          path.setPath(getURIMapping());
          Rest_Endpoint endpoint = new Rest_Endpoint(path);
          return JSON.serialize(endpoint);
        }
    }
}

5. Filtering

Filtering will be passed as key values where key to the Rest_Dispatcher.Dispatchable

GET /accounts?limit=50&offset=10

Execute method will receive filer key/value as parameters

 global Rest_Response execute(Map<String, String> parameters){
            Rest_Response restResponse = new Rest_Response();
            String limitVal = parameters.containsKey('limit') : parameters.get('limit') :null;
            String offsetVal = parameters.containsKey('offset') : parameters.get('offset') : null;
            List<Accounts> accoutsLst = new List<Account>();

            if (limitVal!=null && offsetVal!=null)
             accoutsLst = [Select Id, Name from Account limit :limit limitVal offset :offsetVal];
            else
             accoutsLst = [Select Id, Name from Account];
          
            restResponse.setResult(accoutsLst);
            return restResponse;
        }

6. Security

Security is one of the major concerns when compared to SOAP because still there are no standards such as ws-security defined for REST.

  • HTTPS is the default for all Apex REST API calls
  • Don’t forget to include timestamp in each and every API request and response. Make sure to log them all. In case of a dispute you can refer them.
    • Payload returned has timestamp

    Example of payload the contains timeStamp

    {
        "result": {
            "AccountId": "0014P00002hysSZQAY",
            "Id": "0054P00000C8OBlQAN"
        },
        "timeStamp": "2020-05-06T18:39:43.474Z",
        "totalTime": 0.034,
        "userId": "0054P00000C8OBlQAN",
        "mapKeysFlag": false,
        "serializeNulls": false
    }
    

    7. Analytics

    Log every API call request to build an analytical platform on top of that. Having analytics in your REST API will give you a good insight of what’s happening your API. To detect:
    1. Long running queries
    2. Duplicate requests by same user
    3. Errors by type

    public class Rest_Log {
    
      public static Rest_Log instance {get; private set;}
        
      static {
        instance = new Rest_Log();
      }
    
      private static final String PREFIX = 'Rest Log';
    
      public static HttpRequest setupLogEntries(){
        HttpResponse res=new HttpResponse();
        HttpRequest req = new HttpRequest();
        String logEntricUrl = 'https://webhook.logentries.com/noformat/logs/{logEntriesToken}';
        req.setEndpoint(logEntricUrl);
        req.setTimeout(60000);
        req.setMethod('POST');
        return req;
      }
    
      @future(callout=true)
      public static void putLogCentricLog(String responseCode) {
        try{
          HttpRequest req = setupLogEntries();
           if (responseCode!=null){
             req.setBody(PREFIX + ' : ' + responseCode);
           }
           Http http = new Http();
           http.send(req);
        } catch (System.CalloutException ex){
          System.debug('Callout failed ' + ex.getMessage());
        } catch (Exception ex){
          System.debug('Callout failed ' + ex.getMessage());
        }
      }
    }
    

    Example of the logging every API call to LogEntries (https://elements.heroku.com/addons/logentries)

    11 May 2020 10:20:05.167 Rest Log : Debug UserId: 0054P00000C8OBlQAN | TraceId: null | TotalTime: 0.042 | Method: HTTPGET | Path: /v1/accounts | Body:
    11 May 2020 10:20:10.934 Rest Log : Debug UserId: 0054P00000C8OBlQAN | TraceId: null | TotalTime: 0.007 | Method: HTTPGET | Path: /v1/settings/endpoints | Body:
    

    Example of a dashboard with widgets in LogEntries to monitor response times, errors and slow endpoints

    8. Documentation

    Proper Documentation is vital for the API. It doesn’t matter how great your API design is if the API consumers can’t consume it properly.

    Override toString for each endpoint

    global override String toString(){
              Rest_Endpoint.Path path = new Rest_Endpoint.Path();
              path.setPath(getURIMapping());
              path.setSummary('Get accounts for logged in user');
              path.setProduces(Rest_Endpoint.getModelByObject('Account'));
              Rest_Endpoint endpoint = new Rest_Endpoint(path);
              return JSON.serialize(endpoint);
            }
    

    Convert and return List of Rest Endpoints

     public static List<Rest_Endpoint> convertToEndpoint(Map<RequestType, List<Dispatchable>> dispatchableMap){
              List<Rest_Endpoint> restEndpoints = new List<Rest_Endpoint>();
              for (RequestType dpMap : dispatchableMap.keySet()){
                List<Dispatchable> dispatchableList = dispatchableMap.get(dpMap);
                for (Dispatchable dpList : dispatchableList){
                  Rest_Endpoint endpoint = (Rest_Endpoint)System.JSON.deserialize(dpList.toString(), Rest_Endpoint.class);
                  Rest_Endpoint.Path path = endpoint.getPath();
                  path.setOperation(String.valueOf(dpMap));
                  restEndpoints.add(endpoint);
                }
              }
              return restEndpoints;
            }
    

    Calling GET /settings/endpoints will return api documentation for all endpoints

    {
        "result": [
            {
                "path": {
                    "operation": "HTTPGET",
                    "produces": {
                        "Ownership_Check__c": "ownershipCheck",
                        "Total_Assets__c": "totalAssets",
                        "Number_of_Contacts__c": "numberofContacts",
                        "Match_Billing_Address__c": "matchBillingAddress",
                        "SLAExpirationDate__c": "sLAExpirationDate",
                        "SLASerialNumber__c": "sLASerialNumber",
                        "UpsellOpportunity__c": "upsellOpportunity",
                        "NumberofLocations__c": "numberofLocations",
                        "Active__c": "active",
                        "SLA__c": "sLA",
                        "CustomerPriority__c": "customerPriority",
                        "SicDesc": "sicDesc",
                        "AccountSource": "accountSource",
                        "Jigsaw": "jigsaw",
                        "IsCustomerPortal": "isCustomerPortal",
                        "IsPartner": "isPartner",
                        "Site": "site",
                        "Rating": "rating",
                        "Description": "description",
                        "TickerSymbol": "tickerSymbol",
                        "Ownership": "ownership",
                        "NumberOfEmployees": "numberOfEmployees",
                        "AnnualRevenue": "annualRevenue",
                        "Industry": "industry",
                        "Sic": "sic",
                        "Website": "website",
                        "AccountNumber": "accountNumber",
                        "Fax": "fax",
                        "Phone": "phone",
                        "ShippingLongitude": "shippingLongitude",
                        "ShippingLatitude": "shippingLatitude",
                        "ShippingCountry": "shippingCountry",
                        "ShippingPostalCode": "shippingPostalCode",
                        "ShippingState": "shippingState",
                        "ShippingCity": "shippingCity",
                        "ShippingStreet": "shippingStreet",
                        "BillingLongitude": "billingLongitude",
                        "BillingLatitude": "billingLatitude",
                        "BillingCountry": "billingCountry",
                        "BillingPostalCode": "billingPostalCode",
                        "BillingState": "billingState",
                        "BillingCity": "billingCity",
                        "BillingStreet": "billingStreet",
                        "ParentId": "parentId",
                        "Type": "type",
                        "Name": "name"
                    },
                    "summary": "Get account for logged in user",
                    "uri": "/{version}/accounts"
                }
            }
        ],
        "timeStamp": "2020-05-11T23:43:51.063Z",
        "totalTime": 0.202,
        "userId": "0054P00000C8OBlQAN",
        "mapKeysFlag": false,
        "serializeNulls": false
    }
    
  • Apex extend standard objects to add business logic

    Currently it is not possible to extend a standard or custom object in Salesforce. Why would you want to do that you may ask, think about the following, how would you check the age of a contact, if they have a mobilephone, sort list of objects, serialize, deserialize, .

    Contact con = [Select Birthdate from Contact][0];
    Integer conAge = con.Birthdate.daysBetween(Date.Today())/365;
    
    if (conAge > 35){
    ...
    }
    
    let's abstract the business logic to isAgeGreater(35) method: cleaner and reusable for any age comparison. 
    
    Contact con = [Select Birthdate from Contact][0];
    Obj.IContact icontact = new Obj.IContact(con);
    if (icontact.isAgeGreater(35)){
    ...
    }
    

    Define an Obj class that will have inner classes for all the standard objects that needs extra business logic.
    1. Constructor for 1 object
    2. Constructor for List of objects
    3. Methods with business logic getAge, hasMobileNumber, add more if needed.
    4. Sorting by any field by assigning SORT_BY
    5. Sorting by any direction by assigning SORT_DIRECTION (DESC, ASC)
    6. Checking if two objects are equals
    7. Serialize object
    8. Deserialize object

    public class Obj {
         
        public static String NAME_SORT = 'Name';
        public static String EMAIL_SORT = 'Email';
        public static String SORT_BY = NAME_SORT;
        
        public static String SORT_DESC = 'DESC';
        public static String SORT_ASC = 'ASC';
        public static String SORT_DIRECTION = SORT_ASC;
        
        public class IContact implements Comparable{
    
            private Contact con;
            private List<Contact> cons;
            private List<IContact> conLst;
            
            public IContact(List<Contact> cons){
                this.cons = cons;
                this.conLst = new List<IContact>();
                for (Contact con : cons){
                    conLst.add(new IContact(con));
                }
            }
            
            public IContact(Contact con){
                this.con = con;
            }
            
            public Contact getContact(){
                return con;
            }
            
            public Boolean hasMobile(){
                return con.MobilePhone!=null;
            } 
            
            public Integer getAge(){
                return con.Birthdate!=null ? (con.Birthdate.daysBetween(Date.Today())/365) : 0;
            }
    
            public Boolean isAgeGreater(Integer compareAge){
                Integer contactAge = getAge();
                return contactAge > 0 ? compareAge < contactAge : false;
            }
            
            public List<IContact> getList(){
                return conLst;
            }
            
            public List<IContact> sort(){
               conLst.sort();
               return conLst;
            }
    
            private Integer sortByName(Contact compare){
                Integer compVal = 0;
                 if (compare.Name < con.Name){
                    return SORT_DIRECTION.equals(SORT_ASC) ? 1 : -1;
                } else if (con.Name < compare.Name){
                    return SORT_DIRECTION.equals(SORT_ASC) ? -1 : 1;
                }
                return compVal;
            }
            
             private Integer sortByEmail(Contact compare){
                Integer compVal = 0;
                 if (compare.Email < con.Email){
                    return SORT_DIRECTION.equals(SORT_ASC) ? 1 : -1;
                } else if (con.Email < compare.Email){
                    return SORT_DIRECTION.equals(SORT_ASC) ? -1 : 1;
                }
                return compVal;
            }
            
            public Integer compareTo(Object obj){
                if (SORT_BY.equals(NAME_SORT)){
                    Contact compare = ((IContact)obj).con;
                    return sortByName(compare);
                } else if (SORT_BY.equals(EMAIL_SORT)){
                    Contact compare = ((IContact)obj).con;
                    return sortByEmail(compare);
                }
                return 0;
            } 
            
            public Boolean isEquals(Contact concompare){
                return System.equals(con, concompare);
            }
            
            public Integer getHashCode(){
                return System.hashCode(con);
            }   
            
            public String serialize(){
                return JSON.serialize(this);
            }
            
            public IContact deserialize(String jsonCon){
                return (IContact)JSON.deserialize(jsonCon, IContact.class);
            }
        }   
    }
    

    Testing standard object Wrapper

    List<Contact> contacts = [Select Id, Name, MobilePhone, Birthdate from Contact];
    Obj.SORT_BY = Obj.NAME_SORT;
    Obj.SORT_DIRECTION = Obj.SORT_ASC;
    List<Obj.IContact> icontacts = new Obj.IContact(contacts).sort();
    for (Obj.IContact c : icontacts){
        System.debug(c.hasMobile());
        System.debug(c.getAge());  
        System.debug(c.getHashCode());
        System.debug(c.isEquals(c.getContact()));
    }
    

    Apex Max Cases Per Account Trigger

    Fun coding challenge from:
    https://www.sfdc99.com/2014/11/10/show-me-the-code-extravagant-record-creation/

    Max cases Trigger

    trigger MaxCaseTrigger on Case (before insert) {
        CaseHelper ch = new CaseHelper();
        ch.checkMaxCase(Trigger.new);
    }
    

    Max cases helper class

    public with sharing class CaseHelper {
        
        private static Case_Settings__c caseSettings;
        private static Integer maxCases = 99;
        
        static {
         Map<String, Case_Settings__c> setting = Case_Settings__c.getall();
         caseSettings = setting.get('Sandbox_Settings');
         
         if (caseSettings!=null && caseSettings.Max_Cases__c!=null){
             maxCases = Integer.valueOf(caseSettings.Max_Cases__c);
         }
        }
        
        public void checkMaxCase(List<Case> newCases){
            Map<Id, Case> accountIdToCaseMap = new Map<Id, Case>();
            for (Case newCase :newCases){
                if (newCase.AccountId!=null)
                    accountIdToCaseMap.put(newCase.OwnerId, newCase);
            }
            
            List<AggregateResult> agList = [Select count(id) caseCount, OwnerId, Owner.Name from Case where OwnerId IN :accountIdToCaseMap.keySet() Group By OwnerId, Owner.Name Having count(id)>=:maxCases];
            
            List<Case> casesToThrowError = new List<Case>();
            Map<Id, String> caseToContactNameMap = new Map<Id, String>();
            for (AggregateResult ag : agList){
                String ownerId = (String)ag.get('OwnerId');
                if (accountIdToCaseMap.containsKey(ownerId)){
                    Case errorCase = accountIdToCaseMap.get(ownerId);
                    casesToThrowError.add(errorCase);
                    caseToContactNameMap.put(ownerId, (String)ag.get('Name'));
                }
            }
             
            if (!casesToThrowError.isEmpty()){
                for (Case caseToThrowError : casesToThrowError){
                    caseToThrowError.addError('Too many cases created this month for user ' + caseToContactNameMap.get(caseToThrowError.OwnerId) + ' (' + caseToThrowError.OwnerId +  ') : ' + maxCases);
                }
            }
        }
    }
    

    Max cases helper test

    @isTest
    public class CaseHelperTest {
    
        static Integer maxCasesToCreate = 99;
        
        @TestSetup static void setup(){
            Case_Settings__c caseSettings = new Case_Settings__c(Name='Sandbox_Settings', Max_Cases__c=maxCasesToCreate);
            insert caseSettings;
        
            Account newAccount = new Account(Name='Test');
            insert newAccount;
            
            Contact newContact = new Contact(FirstName='First', LastName='Last', AccountId=newAccount.Id, Email='test@google.com');
            insert newContact;
        
            List<Case> caseLst = new List<Case>();
            
            for (Integer k = 0; k < maxCasesToCreate-1; k++){
                Case newCase = new Case();
                newCase.OwnerId = UserInfo.getUserId();
                newCase.AccountId = newAccount.Id;
                newCase.ContactId = newContact.Id;
                newCase.Status = 'Open';
                caseLst.add(newCase);
            }
            
            insert caseLst;
        }
    
         @isTest static void belowThreshold(){
            Case_Settings__c cs = Case_Settings__c.getInstance('Sandbox_Settings');
            cs.Max_Cases__c = 101;
            update cs;
            
            Test.startTest();
                System.assertEquals([Select Id from Case where ContactId!=null].size(), 98);
                Case newCase = new Case();
                newCase.OwnerId = UserInfo.getUserId();
                newCase.AccountId = [Select Id from Account][0].Id;
                newCase.ContactId = [Select Id from Contact][0].Id;
                newCase.Status = 'Open';
                insert newCase;
            Test.stopTest();
        }
    
        @isTest static void errorAboveThreshold(){
            Test.startTest();
                System.assertEquals([Select Id from Case where ContactId!=null].size(), 98);
                Case newCase = new Case();
                newCase.AccountId = [Select Id from Account][0].Id;
                newCase.ContactId = [Select Id from Contact][0].Id;
                newCase.Status = 'Open';
                try{
                    insert newCase;        
                } catch(Exception ex){         
                    System.assert(ex.getMessage().contains('Too many cases created this month for user'));
                }
            Test.stopTest();
        }
    }
    

    Cracking the Coding Interview: Unscripted Interview Video Solutions

    Anagram word search problem

    import java.util.*;
    
    class AnagramWordSearch {
    	
    	private static Set<String> dictionarySet;
    	private static Map<String, Set<String>> anagramMap;
    	
    	static {
            //Not optimal to load every time would want to cache the anagramMap
    		dictionarySet = loadDictionary();
    		anagramMap = buildWordMatchMap();
    	}
    	
    	public static Set<String> loadDictionary(){
    		dictionarySet = new HashSet<>(Arrays.asList("may", "student","students","dog","studentssess","god", "cat", "act", "tab", "bat", "flow","wolf", "lambs","amy", "yam", "balms", "looped", "poodle"));
    		return dictionarySet;
    	}
    	
    	public static Map<String, Set<String>> buildWordMatchMap(){
    		anagramMap = new HashMap<>();
    		for (String dictionaryWord : dictionarySet){
    			char[] chars = dictionaryWord.toCharArray();
    			Arrays.sort(chars);
    			String sortDictionaryWord = new String(chars);
    			if (anagramMap.containsKey(sortDictionaryWord)){
    				Set<String> existingWordMatch = anagramMap.get(sortDictionaryWord);
    				existingWordMatch.add(dictionaryWord);
    				anagramMap.put(sortDictionaryWord, existingWordMatch);
    			} else {
    				Set<String> newWordMatchSet = new HashSet<>(Arrays.asList(dictionaryWord));
    				anagramMap.put(sortDictionaryWord, newWordMatchSet);
    			}
    		}
    		return anagramMap;
    	}
    	
    	public static Set<String> getWordsWithSameChars(String word){
    		char[] wordChars = word.toCharArray();
    		Arrays.sort(wordChars);
    		String sortedWord = new String(wordChars);
    		return anagramMap.get(sortedWord);
    	}
    
    	public static void main(String[] args) {
    		Set<String> wordMatchSameChars = getWordsWithSameChars("cat"); 
    		System.out.println(wordMatchSameChars); //cat, act
    	}
    }
    

    Get derivative for Term

    import java.util.*;
    
    class Derivate {
    	
    	public static List<Term> getDerivative(List<Term> polynomials){
    		List<Term> derivativeTerm = new ArrayList<>();
    		for (Term polynomial : polynomials){
    			int expVal = polynomial.getExponent();
    			if (expVal>0){
    				Term newTerm = new Term(polynomial.getCoefficient() * expVal, expVal-1);
    				derivativeTerm.add(newTerm);
    			}
    		}
    		return derivativeTerm;
    	}
    	
    	public static void main(String[] args) {
    		List<Term> terms = new ArrayList<>(Arrays.asList(new Term(3, 2), new Term(5, 1), new Term(9, 0)));
    		System.out.println(getDerivative(terms)); //[{6, 1}, {5, 0}]
    	}
    
        public static class Term {
    		private int exponent;
    		private int coefficient;
    		
    		public Term(int coefficient, int exponent){
    			this.exponent = exponent;
    			this.coefficient = coefficient;
    		}
    		
    		public int getCoefficient(){
    			return coefficient;
    		}
    		
    		public void setCoefficient(int coefficient){
    			this.coefficient = coefficient;
    		}
    		
    		
    		public int getExponent(){
    			return exponent;
    		}
    		
    		public void setExponent(int exponent){
    			this.exponent = exponent;
    		}
    		
    		public String toString(){
    			return "{" + coefficient + ", " + exponent + "}";
    		}
    	}
    
    }
    

    Non Consecutive sequence with the largest sum

    import java.util.*;
    
    class HighestSumOfNonConsecutiveValues {
    	
    	public static Integer setOfConsecutiveElements(int[] intArr){
    		
    		Set<Integer> setOfValues = new HashSet<>();
    		for (int k = 0; k < intArr.length;k++){
    			setOfValues.add(intArr[k]);
    		}
    		
    		Integer largestSum = 0;
    		for (Integer setOfValue : setOfValues){
    			Integer iteratorSum = setOfValue;
    			for (Integer k = 1; k < setOfValues.size();k++){
    				if (setOfValues.contains(setOfValue+k)){
    					iteratorSum += (setOfValue+k);
    				} else if ((setOfValues.contains(setOfValue-k))){
    					iteratorSum += (setOfValue-k);
    				} else {
    					break;
    				}
    			}
    			if (largestSum < iteratorSum){
    				largestSum = iteratorSum;
    			}
    		}
    		
    		return largestSum;
    	}
    	
    	public static void main(String[] args) {
    		int[] arr = new int[]{1,3,5,6,12,11,62,7,101,102,41,37,2,52,2,31,5,69,71,32,33,7,100,11,4};
    		System.out.println(setOfConsecutiveElements(arr)); //303 (100+101+102)
    	} 
    }
    

    Ransome note

     public class Solution {
    
        private static final String YES = "Yes";
        private static final String NO = "No";
    
        static void checkMagazine(String[] magazine, String[] note) {
            Map<String, Integer> notesMap = new HashMap<>();
            for (String n : note){
                if (notesMap.containsKey(n)){
                    Integer wordFreq = notesMap.get(n) + 1;
                    notesMap.put(n, wordFreq);
                } else {
                    notesMap.put(n, 1);
                }
            }
    
            for (String m : magazine){
                if (notesMap.containsKey(m)){
                    Integer currentFreq = notesMap.get(m);
                    if (1 < currentFreq){
                        currentFreq-=1;
                        notesMap.put(m, currentFreq);
                    } else {
                        notesMap.remove(m);
                        if (notesMap.isEmpty()){
                            break;
                        }
                    }
                } 
            }
    
            String foundAllWordsInMagazine = YES;
            if (!notesMap.isEmpty()){
                foundAllWordsInMagazine = NO;
            } 
    
            System.out.println(foundAllWordsInMagazine);
        }
    }