Using the salesforce.com adapter
Contents
So this is seeing the salesforce.com adapter in action:

I have attached a zip file with source code and sample data for the To Translator component for this channel which includes the salesforce.com adapter here: SalesforceTranslator.zip
Now the great thing about this adapter is that it’s really snappy. That’s because of the SQLite caching. It’s interesting to try turning setting the clear_cache flag to true. It’s like night and day in terms of the responsiveness of the editor. With caching it’s blindingly fast and easy to do your work.
Caching does have some drawbacks – I picked a default time of 60 seconds. Cached data can be out of date – if you don’t keep it in mind it could result in unexpected results like if you do a query of data after deleting an object then it will still be there. I made it possible to override the default cache time for each query call into the adapter.
Another really neat thing about the salesforce.com module is that once you take away the meta definitions for each of the salesforce objects is that it is less than 300 lines of code. The meta definitions themselves make up the majority of the code. It makes the whole thing a breeze to maintain since there is so little code and any changes in the salesforce.com objects can easily expressed by updating the meta defintions. Beautiful!
You will need to spend some time making your comfortable with the salesforce.com data model. One interesting tit-bit I found useful was this little line:
local QueueList = S:groupList{where=“Type = ‘Queue’”}
This gives you a list of all the user defined ticket “queues” which is a workflow concept used a lot in salesforce.com. It’s not 100% obvious from the API, but I find with salesforce.com there is a lot of good information one can get via Google for these types of questions. One page I would recommend bookmarking is the object model of salesforce.com which I found had great information.
So to finish things off I will walk through how we do the CRUD operations with this API and also how to get a fresh copy of the meta data to modify, add or remove which of the salesforce.com objects we want to expose. In general I think with most salesforce.com implementations most companies will select a subset of salesforce.com to work with – it’s the old 80-20 rule.
So doing queries with this API is very convenient. The “List” methods take the optional arguments of:
- limit – this is the maximum number of records to return.
- where – this is SOQL (SQL) like WHERE clause to use to filter the results.
- cache – this allows one to override the default time of 60 seconds to another time threshold – or 0 for no caching.
I guess one could easily enhance my implementation to add an optional order by clause too. This screenshot shows the results coming back from a list of contacts:

Notice the custom fields “MRN__c” and “Languages__c”?
Creating and updating records is done using the xxxxModify{} methods. A new record will be created if the “id” field is not present, if “id” is present than an existing object will be updated. salesforce.com uses unique GUIDs for every object in the database.
The modify methods are really easy to use since the properties of the object are all expressed as optional parameters. This screenshot gives you an idea of how it works:

And delete methods are really obvious – xxxxDelete{id=<id of object to delete>}. Both the modify and delete methods support the “live” flag. By default in the editor the live flag is set to false since that would alter the salesforce.com application in the editor which one usually doesn’t want.
Now I deliberately left out a lot of the salesforce.com objects since I think many are probably not going used by most organizations. But it’s dead easy to add them. To do it just use the handy apiDefinition method I have supplied. If you were to do say, S:apiDefinition(‘Idea’) then this is what you would get back from that method:
objectDefs.idea = {object='Idea', fields={
Categories="?",
LastModifiedDate="?",
LastCommentDate="?",
CreatedDate="?",
Body="?",
LastCommentId="?",
CreatedById="?",
LastModifiedById="?",
Status="?",
SystemModstamp="?",
IsHtml="?",
NumComments="?",
ParentIdeaId="?",
RecordTypeId="?",
IsLocked="?",
Title="?",
CommunityId="?",
VoteTotal="?",
VoteScore="?",
IsDeleted="?"}}
Notice how the descriptions of the parameters come up as ‘?’ This is because the API that salesforce.com does supply the description of for the fields of each object. You’ll have to enter that by hand – still it’s not all that onerous to do that and I find the names that salesforce.com selected were all pretty easy to understand.
I put some smarts into the code so that if the existing meta definitions have a description of a parameter that the apiDefinition method re-uses those descriptions which is nice feature to have. So if you put descriptions in for fields you won’t lose them if you refresh the definition.
If you have any questions or comments about the code and the techniques it uses do feel free to ask. This is my first draft of the article too, so if you spot any typos then let me know. Here’s the salesforce module:
salesforce = {}
store = require 'store'
objectDefs = {}
objectDefs.contact = {object='Contact', fields={
OtherPhone="Alternative phone number",
FirstName="First name of contact",
AssistantPhone="Their secretary's phone",
MailingState="State mailing address",
MRN__c="MRN number",
Salutation="How to greet them",
Languages__c="Languages they speak",
Phone="Phone number of the contact",
Email="Email of contact",
AccountId="AccountID of the contact",
Birthdate="Birthdate of the contact",
Description="Description of the contact",
HomePhone="Home phone number",
LastName="Last name of contact",
LastActivityDate="Last activity date",
CreatedById="Who created this",
LastModifiedDate="When last modified",
LastModifiedById="Who modified this",
CreatedDate="Date created",
MailingPostalCode="Postal code for mailing address",
Department="What department",
MobilePhone="Their mobile phone",
MailingCity="City to mail to.",
MailingStreet="Street to mail to",
OwnerId="Owner of this contact",
IsDeleted="Has this contact been deleted",
Fax="FAX number",
Title="Title of contact - CEO etc."
}}
objectDefs.case = {object='Case', fields={
SuppliedPhone="Supplied phone number",
Description="Description",
Origin="Origin of the phone",
CreatedDate="When created",
IsEscalated="Has this been escalated",
ClosedDate="When closed",
Reason="Reason for ticket",
CaseNumber="Case number",
CreatedById="Who created it",
LastModifiedById="?",
Status="?",
Priority="?",
SuppliedEmail="?",
SuppliedName="?",
ParentId="?",
ContactId="?",
OwnerId="?",
IsDeleted="?",
Subject="?",
SystemModstamp="?",
LastModifiedDate="?",
IsClosed="?",
SuppliedCompany="?",
AccountId="?"}}
objectDefs.queueSobject = {object='QueueSobject', fields={
SystemModstamp="A field",
QueueId="A field",
SobjectType="A field",
CreatedById="A field"}}
objectDefs.group = {object='Group', fields={
Type="A field",
LastModifiedDate="A field",
CreatedDate="A field",
DoesIncludeBosses="A field",
Email="A field",
OwnerId="A field",
Name="A field",
LastModifiedById="A field",
RelatedId="A field",
SystemModstamp="A field",
DoesSendEmailToMembers="A field",
CreatedById="A field"}}
objectDefs.note = {object='Note', fields={
IsPrivate="A field",
LastModifiedDate="A field",
CreatedDate="A field",
ParentId="A field",
Body="A field",
OwnerId="A field",
IsDeleted="A field",
CreatedById="A field",
LastModifiedById="A field",
SystemModstamp="A field",
Title="A field"}}
objectDefs.community = {object='Community', fields={
IsActive="A field",
Description="A field",
CreatedById="A field",
LastModifiedById="A field",
CreatedDate="A field",
Name="A field",
SystemModstamp="A field",
LastModifiedDate="A field"}}
objectDefs.contentDocument = {object='ContentDocument', fields={
LastModifiedDate="A field",
CreatedDate="A field",
LatestPublishedVersionId="A field",
OwnerId="A field",
IsDeleted="A field",
Title="A field",
LastModifiedById="A field",
PublishStatus="A field",
SystemModstamp="A field",
CreatedById="A field"}}
objectDefs.event = {object='Event', fields={
IsPrivate="A field",
StartDateTime="A field",
IsArchived="A field",
CreatedDate="A field",
IsAllDayEvent="A field",
ActivityDate="A field",
CreatedById="A field",
RecurrenceInstance="A field",
IsGroupEvent="A field",
RecurrenceEndDateOnly="A field",
ActivityDateTime="A field",
WhatId="A field",
RecurrenceActivityId="A field",
EndDateTime="A field",
Subject="A field",
Location="A field",
IsChild="A field",
AccountId="A field",
RecurrenceType="A field",
Description="A field",
IsRecurrence="A field",
WhoId="A field",
RecurrenceDayOfWeekMask="A field",
RecurrenceStartDateTime="A field",
SystemModstamp="A field",
LastModifiedById="A field",
ReminderDateTime="A field",
RecurrenceDayOfMonth="A field",
DurationInMinutes="A field",
RecurrenceMonthOfYear="A field",
OwnerId="A field",
IsDeleted="A field",
ShowAs="A field",
RecurrenceInterval="A field",
RecurrenceTimeZoneSidKey="A field",
IsReminderSet="A field",
LastModifiedDate="A field",
GroupEventType="A field"}}
objectDefs.user = {object='User', fields={
UserPreferencesApexPagesDeveloperMode="A field",
FirstName="A field",
Division="A field",
FederationIdentifier="A field",
Alias="A field",
UserPreferencesHideCSNDesktopTask="A field",
DelegatedApproverId="A field",
EmailEncodingKey="A field",
LastLoginDate="A field",
UserType="A field",
OfflineTrialExpirationDate="A field",
UserPermissionsKnowledgeUser="A field",
CallCenterId="A field",
EmployeeNumber="A field",
City="A field",
LastModifiedById="A field",
UserPermissionsInteractionUser="A field",
CommunityNickname="A field",
MobilePhone="A field",
Extension="A field",
ContactId="A field",
State="A field",
UserRoleId="A field",
SmallPhotoUrl="A field",
Fax="A field",
UserPermissionsOfflineUser="A field",
UserPermissionsMobileUser="A field",
UserPreferencesEventRemindersCheckboxDefault="A field",
ForecastEnabled="A field",
UserPreferencesHideCSNGetChatterMobileTask="A field",
LastPasswordChangeDate="A field",
UserPreferencesDisableAutoSubForFeeds="A field",
Country="A field",
Phone="A field",
DigestFrequency="A field",
Email="A field",
UserPreferencesActivityRemindersPopup="A field",
ReceivesInfoEmails="A field",
AboutMe="A field",
UserPermissionsCallCenterAutoLogin="A field",
CompanyName="A field",
LastModifiedDate="A field",
ReceivesAdminInfoEmails="A field",
LastName="A field",
Street="A field",
Username="A field",
FullPhotoUrl="A field",
CurrentStatus="A field",
ProfileId="A field",
CreatedDate="A field",
AccountId="A field",
SystemModstamp="A field",
OfflinePdaTrialExpirationDate="A field",
Department="A field",
CreatedById="A field",
UserPreferencesOptOutOfTouch="A field",
PostalCode="A field",
UserPreferencesReminderSoundOff="A field",
LanguageLocaleKey="A field",
IsActive="A field",
UserPermissionsSupportUser="A field",
LocaleSidKey="A field",
UserPreferencesTaskRemindersCheckboxDefault="A field",
UserPermissionsSFContentUser="A field",
TimeZoneSidKey="A field",
ManagerId="A field",
UserPermissionsMarketingUser="A field",
Title="A field",
Name="A field"}}
objectDefs.product2 = {object='Product2', fields={
Description="A field",
CreatedDate="A field",
ProductCode="A field",
IsActive="A field",
IsDeleted="A field",
Name="A field",
LastModifiedById="A field",
Family="A field",
CreatedById="A field",
SystemModstamp="A field",
LastModifiedDate="A field"}}
objectDefs.taskStatus = {object='TaskStatus', fields={
LastModifiedDate="A field",
CreatedDate="A field",
MasterLabel="A field",
SortOrder="A field",
CreatedById="A field",
LastModifiedById="A field",
IsDefault="A field",
IsClosed="A field",
SystemModstamp="A field"}}
objectDefs.document = {object='Document', fields={
Description="A field",
CreatedDate="A field",
Body="A field",
NamespacePrefix="A field",
LastModifiedById="A field",
Url="A field",
SystemModstamp="A field",
AuthorId="A field",
BodyLength="A field",
DeveloperName="A field",
Keywords="A field",
LastModifiedDate="A field",
CreatedById="A field",
IsInternalUseOnly="A field",
IsDeleted="A field",
Name="A field",
Type="A field",
IsPublic="A field",
FolderId="A field",
ContentType="A field",
IsBodySearchable="A field"}}
objectDefs.taskPriority = {object='TaskPriority', fields={
LastModifiedDate="A field",
CreatedDate="A field",
MasterLabel="A field",
SortOrder="A field",
CreatedById="A field",
LastModifiedById="A field",
IsHighPriority="A field",
SystemModstamp="A field",
IsDefault="A field"}}
objectDefs.idea = {object='Idea', fields={
Categories="?",
LastModifiedDate="?",
LastCommentDate="?",
CreatedDate="?",
Body="?",
LastCommentId="?",
CreatedById="?",
LastModifiedById="?",
Status="?",
SystemModstamp="?",
IsHtml="?",
NumComments="?",
ParentIdeaId="?",
RecordTypeId="?",
IsLocked="?",
Title="?",
CommunityId="?",
VoteTotal="?",
VoteScore="?",
IsDeleted="?"}}
objectDefs.task = {object='Task', fields={
IsArchived="A field",
CreatedDate="A field",
ActivityDate="A field",
CreatedById="A field",
RecurrenceInstance="A field",
Priority="A field",
RecurrenceStartDateOnly="A field",
CallType="A field",
WhatId="A field",
CallDurationInSeconds="A field",
Subject="A field",
ReminderDateTime="A field",
RecurrenceInterval="A field",
AccountId="A field",
RecurrenceType="A field",
Description="A field",
IsRecurrence="A field",
WhoId="A field",
RecurrenceDayOfWeekMask="A field",
CallObject="A field",
Status="A field",
SystemModstamp="A field",
RecurrenceTimeZoneSidKey="A field",
RecurrenceDayOfMonth="A field",
LastModifiedDate="A field",
RecurrenceMonthOfYear="A field",
CallDisposition="A field",
IsDeleted="A field",
LastModifiedById="A field",
IsClosed="A field",
RecurrenceEndDateOnly="A field",
IsReminderSet="A field",
RecurrenceActivityId="A field",
OwnerId="A field"}}
objectDefs.account = {object='Account', fields={
NumberofLocations__c="A field",
CreatedDate="A field",
UpsellOpportunity__c="A field",
Industry="A field",
BillingCountry="A field",
AnnualRevenue="A field",
Sic="A field",
Type="A field",
TickerSymbol="A field",
Phone="A field",
ShippingStreet="A field",
NumberOfEmployees="A field",
SLA__c="A field",
AccountNumber="A field",
Description="A field",
ShippingPostalCode="A field",
BillingCity="A field",
ShippingCity="A field",
BillingState="A field",
SLASerialNumber__c="A field",
BillingPostalCode="A field",
LastModifiedById="A field",
Active__c="A field",
CustomerPriority__c="A field",
SystemModstamp="A field",
Website="A field",
LastModifiedDate="A field",
CreatedById="A field",
ShippingState="A field",
Site="A field",
MasterRecordId="A field",
Rating="A field",
ParentId="A field",
LastActivityDate="A field",
OwnerId="A field",
IsDeleted="A field",
Name="A field",
Ownership="A field",
ShippingCountry="A field",
Fax="A field",
SLAExpirationDate__c="A field",
BillingStreet="A field"}}
local function GetCache(Key, CacheTimeout)
if (CacheTimeout == 0) then
return nil
end
local CacheTime = store.get(Key.."T")
if (os.ts.difftime(os.ts.time(), CacheTime) < CacheTimeout) then
local CachedData = store.get(Key)
local R = json.parse{data=CachedData}
return R
end
return nil
end
local function PutCache(Key, Value)
store.put(Key, Value)
store.put(Key.."T", os.ts.time())
end
-- We only cache in development
local function cachedHttpGet(T)
local R, Key
if iguana.isTest() then
Key = T.url;
for K,V in pairs(T.parameters) do
Key = Key..K..V
end
trace(Key)
R = GetCache(Key, T.cache or 60)
if (R) then return R end
end
T.cache = nil;
local P = net.http.get(T)
R = json.parse{data=P}
if iguana.isTest() then
PutCache(Key, P)
end
return R
end
local function GetAccessTokenViaHTTP(CacheKey,T)
local Url = 'https://login.salesforce.com/services/oauth2/token'
local Auth = {grant_type = 'password',
client_id = T.consumer_key,
client_secret = T.consumer_secret,
username = T.username,
password = T.password}
local J = net.http.post{url=Url,
parameters = Auth,
live=true}
PutCache(CacheKey, J)
local AccessInfo = json.parse(J)
return AccessInfo
end
local function CheckClearCache(DoClear)
if DoClear then
store.resetTableState()
end
end
local salesmethods = {}
local MetaTable = {}
MetaTable.__index = salesmethods;
function salesforce.connect(T)
CheckClearCache(T.clear_cache)
local P = GetCache(T.consumer_key, 1800) or
GetAccessTokenViaHTTP(T.consumer_key, T)
setmetatable(P, MetaTable)
return P
end
local helpinfo = {} HelpConnect = [[{"SeeAlso":[{"Title":"Salesforce","Link":"http://www.salesforce.com"}],
"Returns":[{"Desc":"The salesforce.com website."}], "Title":"salesforce.connect",
"Parameters":[{"username":{"Desc":"User ID to login with."}}, {"password":{"Desc":"Password of that user ID"}}
{"consumer_key":{"Desc":"Consumer key for this connected app."}}, {"consumer_secret":{"Desc":"Consumer secret for this connected app."}}, {"clear_cache":{"Opt" : true,"Desc":"If this is set to true then then the SQLite cache used to improve performance will be cleared."}},],
"ParameterTable": true,
"Usage":" local C = salesforce.connect{clear_cache=false,
username='sales@interfaceware.com',
password='mypassword',
consumer_secret='585519048400883388',
consumer_key='3MVG9KI2HHAq33RyfdfRmZyEybpy7b_bZtwCyJW7e._mxrVtsrbM.g5n3.fIwK3vPGRl2Ly2u7joju3yYpPeO' }",
"Desc":"Returns a connection object to salesforce instance"}]]
help.set{input_function=salesforce.connect, help_data=json.parse{data=HelpConnect}}
local function ParseResult(Returned)
if #Returned == 0 then
return {}
end
local R = json.parse{data=Returned}
if #R > 0 and R[1].errorCode then
error(R[1].message,4)
end
return R
end
local function patchObject(S, T, ObjectName)
local Live = not iguana.isTest() or T.live
local Path = S.instance_url..
'/services/data/v20.0/sobjects/'..ObjectName..'/'
local Method
if (T.id) then
trace("Updating");
Method = 'PATCH'
Path = Path..T.id
T.id = nil;
else
trace("New record");
Method = 'POST'
end
trace(Path)
T.live = nil;
local Headers={}
Headers['Content-Type']='application/json'
Headers.Authorization ="Bearer ".. S.access_token
local Returned = net.http.put{data=json.serialize{data=T}, method=Method,headers=Headers,
url=Path,live=Live}
return ParseResult(Returned)
end
function salesmethods:describe(Object)
local S = self;
local Url = S.instance_url..'/services/data/v20.0/sobjects/'..Object..'/describe/'
trace(Url)
local Headers={}
Headers['Content-Type']='application/json'
Headers.Authorization ="Bearer ".. S.access_token
return cachedHttpGet({headers=Headers, live=true, url=Url, parameters={}}, 50000)
end
-- Used to generate API set
local function PrettyPrint(List, Name)
local Def = List[Name]
local R = "objectDefs."..Name.." = {"
R = R.."object='"..Def.object.."', fields={\n"
for K,V in pairs(Def.fields) do
R = R..' '..K..'="'..V..'",\n'
end
R = R:sub(1, #R-2).."}}\n\n"
return R
end
function ObjectName(Name)
return Name:sub(1,1):lower()..Name:sub(2)
end
local function GenerateAPI(S, Object)
local Info = S:describe(Object)
local CName = ObjectName(Object)
local Def = {}
Def.object = Object
Def.fields ={}
for i=1, #Info.fields do
local Name = Info.fields[i].name
trace(Name)
trace(Info.fields[i])
if Name ~= 'Id' then
if (objectDefs[CName] and objectDefs[CName].fields[Name] ) then
Def.fields[Name] = objectDefs[CName].fields[Name]
else
Def.fields[Name] = "?"
end
end
end
return Def
end
-- Example objects QueueSobject, Account, Community, Contact, ContentDocument, Document, Product2, Event, Group, Note, Profile, Task, TaskPriority, TaskStatus, User
-- See https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_list.htm
function salesmethods.apiDefinition(S, Name)
local Def = {}
local DefName = ObjectName(Name)
Def[DefName] = GenerateAPI(S, Name)
return PrettyPrint(Def, DefName)
end
function GenerateModifierMethod(Name, Info)
local FName = Name..'Modify'
salesmethods[FName] = function (S,T) return patchObject(S, T, Info.object) end
local F = salesmethods[FName]
local Help = {}
Help.Desc = "Create or update a "..Name
Help.ParameterTable = true
Help.Parameters = {}
Help.Parameters[1] = {id={Opt=true, Desc="Unique id of "..Name..". If not present a new field will be created."}}
Help.Parameters[2] = {live={Opt=true, Desc="Set to true to make this command work in the editor. Default is false."}}
for K,V in pairs(Info.fields) do
Help.Parameters[#Help.Parameters+1] = {}
Help.Parameters[#Help.Parameters][K] = {Opt=true, Desc=V}
end
help.set{input_function=F, help_data=Help}
end
function queryObjects(S, T)
if (T.where) then
T.query = T.query.." WHERE "..T.where
end
if (T.limit) then
T.query = T.query.." LIMIT "..T.limit
end
local P ={parameters={q=T.query}, url=S.instance_url..T.path,
headers={Authorization="Bearer ".. S.access_token}, cache=T.cache, live=true}
local R = cachedHttpGet(P)
if #R > 0 and R[1].errorCode then
CheckClearCache(true)
error(R[1].message,4)
end
return R
end
local function selectQuery(T)
local R = 'SELECT Id';
for K,V in pairs(T.fields) do
R = R..","..K
end
R = R.." FROM "..T.object
return R
end
local function listObjects(S,T,D)
T = T or {}
T.query = selectQuery(D)
T.path = '/services/data/v20.0/query'
return queryObjects(S,T)
end
function GenerateListMethod(Name, Info)
local FName = Name..'List'
salesmethods[FName] = function(S,T) return listObjects(S,T,Info) end;
local F = salesmethods[FName]
local Help = {}
Help.Desc = "Query list of "..Name
Help.ParameterTable = true
Help.Parameters = {}
Help.Parameters[1] = {limit={Opt=true, Desc="Limit the number of results - default is no limit."}}
Help.Parameters[2] = {where={Opt=true, Desc="Give a WHERE clause."}}
Help.Parameters[3] = {cache={Opt=true, Desc="Specific time to cache results (seconds). Default is 60 seconds."}}
help.set{input_function=F, help_data=Help}
end
local function deleteObject(S, T, ObjectName)
local Live = not iguana.isTest() or T.live
local Path = S.instance_url..
'/services/data/v20.0/sobjects/'..ObjectName..'/'..T.id
local Headers={}
Headers['Content-Type']='application/json'
Headers.Authorization ="Bearer ".. S.access_token
local Returned = net.http.put{data=json.serialize{data=T}, method='DELETE',headers=Headers,
url=Path,live=Live}
return ParseResult(Returned)
end
function GenerateDeleteMethod(Name, Info)
local FName = Name..'Delete'
salesmethods[FName] = function (S,T) return deleteObject(S,T,Info.object) end
local F = salesmethods[FName]
local Help = {}
Help.Desc = "Delete a "..Name
Help.ParameterTable = true
Help.Parameters = {}
Help.Parameters[1] = {id={Desc="Unique id of "..Name.." that will be deleted."}}
Help.Parameters[2] = {live={Opt=true, Desc="Set to true to make this command work in the editor. Default is false."}}
help.set{input_function=F, help_data=Help}
end
function BuildMethods(Objects)
for K,V in pairs(Objects) do
GenerateListMethod(K,V)
GenerateModifierMethod(K,V)
GenerateDeleteMethod(K,V)
end
end
BuildMethods(objectDefs)