---
schema_version: "1"
id: cada111b86fca1f495967cb5ed01bbb5fb54165f0fe090ae556a753e0d92967d
title: Loopback Custom Adapter for Salesforce Connect
collection: legacydeveloper
version: current
locale: en-us
portal_url: https://docs.salesforce.com/legacydeveloper/current/en-us/apexcode/apex_connector_example_loopback.md
canonical_url: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_example_loopback.htm
source_site: developer.salesforce.com
content_type: documentation
content_hash: b9c25256eab3b5b98f32a147be92db946535a42fac589e9d62f742d10880c176
description: This example illustrates how to handle filtering in queries. For simplicity, this example connects the Salesforce org to itself as the external system.
---
# Loopback Custom Adapter for Salesforce Connect

This example illustrates how to handle filtering in queries. For simplicity, this example connects the Salesforce org to itself as the external system.

## LoopbackDataSourceConnection Class

/\*\*
 \*   Extends the DataSource.Connection class to enable 
 \*   Salesforce to sync the external systemâ€™s schema 
 \*   and to handle queries and searches of the external data. 
 \*\*/
global with sharing class LoopbackDataSourceConnection 
    extends DataSource.Connection {

    /\*\*
     \*   Constructors.
     \*\*/
    global LoopbackDataSourceConnection(
        DataSource.ConnectionParams connectionParams) {
    }
    global LoopbackDataSourceConnection() {}
    
    /\*\*
     \*   Called when an external object needs to get a list of 
     \*   schema from the external data source, for example when 
     \*   the administrator clicks â€œValidate and Syncâ€ in the 
     \*   user interface for the external data source.   
     \*\*/
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables = 
            new List<DataSource.Table>();        
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.text('Name', 255));
        columns.add(
            DataSource.Column.number('NumberOfEmployees', 18, 0));
        tables.add(
            DataSource.Table.get('Looper', 'Name', columns));
        return tables;
    }
    
    /\*\*
     \*   Called to query and get results from the external 
     \*   system for SOQL queries, list views, and detail pages 
     \*   for an external object thatâ€™s associated with the 
     \*   external data source.
     \*   
     \*   The QueryContext argument represents the query to run 
     \*   against a table in the external system.
     \*   
     \*   Returns a list of rows as the query results.
     \*\*/
    override global DataSource.TableResult 
        query(DataSource.QueryContext context) {
        if (context.tableSelection.columnsSelected.size() == 1 &&
            context.tableSelection.columnsSelected.get(0).aggregation ==
                DataSource.QueryAggregation.COUNT) {
            integer count = execCount(getCountQuery(context));
            List<Map<String, Object\>> countResponse =
                new List<Map<String, Object\>>();
            Map<String, Object\> countRow =
                new Map<String, Object\>();
            countRow.put(
                context.tableSelection.columnsSelected.get(0).columnName,
                count);
            countResponse.add(countRow);
            return DataSource.TableResult.get(context,countResponse);
        } else {
            List<Map<String,Object\>> rows = execQuery(
                getSoqlQuery(context));
            return DataSource.TableResult.get(context,rows);
        }
    }
    
    /\*\*
     \*   Called to do a full text search and get results from
     \*   the external system for SOSL queries and Salesforce
     \*   global searches.
     \*   
     \*   The SearchContext argument represents the query to run 
     \*   against a table in the external system.
     \*   
     \*   Returns results for each table that the SearchContext 
     \*   requested to be searched.
     \*\*/
    override global List<DataSource.TableResult> 
        search(DataSource.SearchContext context) {        
        return DataSource.SearchUtils.searchByName(context, this);
    }

    /\*\*
     \*   Helper method to execute the SOQL query and 
     \*   return the results.
     \*\*/
    private List<Map<String,Object\>> 
        execQuery(String soqlQuery) {
        List<Account> objs = Database.query(soqlQuery, AccessLevel.USER\_MODE);
        List<Map<String,Object\>> rows = 
            new List<Map<String,Object\>>();
        for (Account obj : objs) {
            Map<String,Object\> row = new Map<String,Object\>();
            row.put('Name', obj.Name);
            row.put('NumberOfEmployees', obj.NumberOfEmployees);
            row.put('ExternalId', obj.Id);
            row.put('DisplayUrl', 
                URL.getOrgDomainUrl().toExternalForm() + 
                    obj.Id);
            rows.add(row);
        }
        return rows;
    }

    /\*\*
     \*   Helper method to get aggregate count.
     \*\*/
    private integer execCount(String soqlQuery) {
        integer count = Database.countQuery(soqlQuery, AccessLevel.USER\_MODE);
        return count;
    }

    /\*\*
     \*   Helper method to create default aggregate query.
     \*\*/
    private String getCountQuery(DataSource.QueryContext context) {
        String baseQuery = 'SELECT COUNT() FROM Account';
        String filter = getSoqlFilter('', 
            context.tableSelection.filter);
        if (filter.length() > 0)
            return baseQuery + ' WHERE ' + filter;
        return baseQuery;
    }

    /\*\*
     \*   Helper method to create default query.
     \*\*/
    private String getSoqlQuery(DataSource.QueryContext context) {
        String baseQuery = 
            'SELECT Id,Name,NumberOfEmployees FROM Account';
        String filter = getSoqlFilter('', 
            context.tableSelection.filter);
        if (filter.length() > 0)
            return baseQuery + ' WHERE ' + filter;
        return baseQuery;
    }

    /\*\*
     \*   Helper method to handle query filter.
     \*\*/
    private String getSoqlFilter(String query, 
        DataSource.Filter filter) {
        if (filter == null) {
            return query;
        }
        String append;
        DataSource.FilterType type = filter.type;
        List<Map<String,Object\>> retainedRows = 
            new List<Map<String,Object\>>();
        if (type == DataSource.FilterType.NOT\_) {
            DataSource.Filter subfilter = filter.subfilters.get(0);
            append = getSoqlFilter('NOT', subfilter);
        } else if (type == DataSource.FilterType.AND\_) {
            append =  
                getSoqlFilterCompound('AND', filter.subfilters);
        } else if (type == DataSource.FilterType.OR\_) {
            append = 
                getSoqlFilterCompound('OR', filter.subfilters);
        } else {
            append = getSoqlFilterExpression(filter);
        }
        return query + ' ' + append;
    }
    
    /\*\*
     \*   Helper method to handle query subfilters.
     \*\*/
    private String getSoqlFilterCompound(String operator, 
        List<DataSource.Filter> subfilters) {
        String expression = ' (';
        boolean first = true;
        for (DataSource.Filter subfilter : subfilters) {
            if (first)
                first = false;
            else
                expression += ' ' + operator + ' ';
            expression += getSoqlFilter('', subfilter);
        }
        expression += ') ';
        return expression;
    }
    
    /\*\*
     \*   Helper method to handle query filter expressions.
     \*\*/
    private String getSoqlFilterExpression(
        DataSource.Filter filter) {
        String columnName = filter.columnName;
        String operator;
        Object expectedValue = filter.columnValue;
        if (filter.type == DataSource.FilterType.EQUALS) {
            operator = '=';
        } else if (filter.type == 
            DataSource.FilterType.NOT\_EQUALS) {
            operator = '<>';
        } else if (filter.type == 
            DataSource.FilterType.LESS\_THAN) {
            operator = '<';
        } else if (filter.type == 
            DataSource.FilterType.GREATER\_THAN) {
            operator = '>';
        } else if (filter.type == 
            DataSource.FilterType.LESS\_THAN\_OR\_EQUAL\_TO) {
            operator = '<=';
        } else if (filter.type == 
            DataSource.FilterType.GREATER\_THAN\_OR\_EQUAL\_TO) {
            operator = '>=';
        } else if (filter.type == 
            DataSource.FilterType.STARTS\_WITH) {
            return mapColumnName(columnName) + 
            ' LIKE \\'' + String.valueOf(expectedValue) + '%\\'';
        } else if (filter.type == 
            DataSource.FilterType.ENDS\_WITH) {
            return mapColumnName(columnName) + 
            ' LIKE \\'%' + String.valueOf(expectedValue) + '\\'';
        } else if (filter.type == 
            DataSource.FilterType.LIKE\_) {
            return mapColumnName(columnName) + 
            ' LIKE \\'' + String.valueOf(expectedValue) + '\\'';
        } else {
            throwException(
            'Implementing other filter types is left as an exercise for the reader: ' 
            + filter.type);
        }
        return mapColumnName(columnName) + 
            ' ' + operator + ' ' + wrapValue(expectedValue);
    }
    
    /\*\*
     \*   Helper method to map column names.
     \*\*/
    private String mapColumnName(String apexName) {
        if (apexName.equalsIgnoreCase('ExternalId'))
            return 'Id';
        if (apexName.equalsIgnoreCase('DisplayUrl'))
            return 'Id';
        return apexName;
    }

    /\*\*
    \*   Helper method to wrap expression Strings with quotes.
    \*\*/
    private String wrapValue(Object foundValue) {
        if (foundValue instanceof String)
            return '\\'' + String.valueOf(foundValue) + '\\'';
        return String.valueOf(foundValue);
    }
}

## LoopbackDataSourceProvider Class

/\*\*
 \*   Extends the DataSource.Provider base class to create a 
 \*   custom adapter for Salesforce Connect. The class informs 
 \*   Salesforce of the functional and authentication 
 \*   capabilities that are supported by or required to connect 
 \*   to an external system.
 \*\*/
global with sharing class LoopbackDataSourceProvider 
    extends DataSource.Provider {
    
    /\*\*
     \*   Declares the types of authentication that can be used 
     \*   to access the external system.
     \*\*/
    override global List<DataSource.AuthenticationCapability> 
        getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities = 
            new List<DataSource.AuthenticationCapability>();
        capabilities.add(
            DataSource.AuthenticationCapability.ANONYMOUS);
        capabilities.add(
            DataSource.AuthenticationCapability.BASIC);
        return capabilities;
    }

    /\*\*
     \*   Declares the functional capabilities that the 
     \*   external system supports.
     \*\*/
    override global List<DataSource.Capability> 
        getCapabilities() {
        List<DataSource.Capability> capabilities = 
            new List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW\_QUERY);
        capabilities.add(DataSource.Capability.SEARCH);
        return capabilities;
    }

    /\*\*
     \*   Declares the associated DataSource.Connection class.
     \*\*/
    override global DataSource.Connection 
        getConnection(DataSource.ConnectionParams connectionParams) {
        return new LoopbackDataSourceConnection();
    }
}