In deze use case is mij gevraagd of ik data vanuit een website CMS, in mijn geval Drupal, kan inladen in Salesforce en op basis van deze data via een juiste mapping een custom object kan vullen. De data is als JSON gestructureerd. Laten we eens kijken naar de mogelijkheden hierbij.
De aangeleverde JSON
Vanuit de form builder in Drupal kan er een JSON gegenereerd worden die er als volgt uitziet (zie foto). Op zich niet heel spannend, maar belangrijk om mee te nemen is dat er een Formdata object in zit, waarin vervolgens de velden van het ingevulde formulier zit. Aangezien we zowel waardes nodig hebben binnen het Formdata object, maar ook daarbuiten, zullen we hier met het schrijven van de APEX-code rekening mee moeten houden.
Salesforce Workbench vs Postman
Salesforce Workbench is een tool die veel eigenschappen heeft, zoals het bijwerken, massaal aanmaken en verwijderen van records, en het uitvoeren van SOQL-query’s, implementeren en debuggen van applicaties, en resetten van gebruikerswachtwoorden. Daarnaast kan het geavanceerde functies uitvoeren zoals SOSL-zoekopdrachten, asynchrone SOQL-query’s en het ophalen van broncode voor aangepaste applicaties. Waar ik Workbench voor ga gebruiken is de REST Explorer die te vinden is onder de utilities tab. Wat veel mensen niet weten is dat Workbench geen tool is van Salesforce zelf.
Postman
Een andere veel gebruikte tool in API testing te doen is Postman. Postman is een platform voor API-ontwikkeling en -testen en biedt diverse functies, zoals het eenvoudig creëren en delen van API-verzoeken en -collecties, het automatiseren van testen en het monitoren van prestaties. Daar waar Workbench direct werkt met Salesforce moet Postman eerst geconnect worden aan de Salesforce instance. Om tijd te besparen en omdat het over een snelle gaat gaat, heb ik voor nu gekozen om Workbench te gebruiken. Mocht er interesse zijn voor een tutorial over Postman, laat maar weten in de comments.
Vereenvoudig de JSON
Om overzichtelijk te blijven werken, heb ik eerst de JSON wat vereenvoudigd, zodat wel de structuur hetzelfde blijft, maar er minder velden in staan (alleen de verplichte velden). Hierbij de JSON die ik gebruik om te kijken of de test succesvol is:
{
"drupalId": "95ee1330-1da2-11ef-a3e9-dbbae47476b7",
"formData": {
"name": "Bezoeker REST test 3",
"item": "a54AW000007BWGfYAO",
"achternaam": "Achternaamtest",
"email": "sdfsdf@dfgsdfg.nl"
}
}
APEX via de Developer Console
Daar waar ik in dit artikel aangegeven heb om met een IDE te werken, ga ik deze keer toch voor de Developer Console. Dit omdat het een snelle test is om te kijken of de aangeleverde data werkbaar is. Maar het is altijd beter om een IDE te gebruiken bij voldoende tijd. Voor nu starten we de Developer Console op door in Salesforce op Setup > Developer Console te klikken. Vervolgens klik je op File > New > APEX Class en de code kan beginnen. Het is de bedoeling dat de JSON data in een custom object, genaamd'Evenement_bezoeker__c'
terecht komt. Ik zal eerst de volledige code plaatsen en die gaan we vervolgens ontleden en verder toelichten.
@RestResource(urlMapping='/getDataFromOurSystem/*')
global with sharing class MyRestResource {
public class FormData {
public String name;
public String achternaam;
public String email;
public String item;
}
@HttpPost
global static void doPost () {
RestRequest req = RestContext.request;
Blob body = req.requestBody;
String requestBody = body.toString();
Map requestData = (Map)JSON.deserializeUntyped(requestBody);
String drupalId = (String)requestData.get('drupalId');
Map formDataMap = (Map)requestData.get('formData');
FormData formData = new FormData();
formData.name = (String)formDataMap.get('name');
formData.achternaam = (String)formDataMap.get('achternaam');
formData.email = (String)formDataMap.get('email');
formData.item = (String)formDataMap.get('item');
Evenement_bezoeker__c bezoeker = new Evenement_bezoeker__c();
bezoeker.Name = formData.name;
bezoeker.Achternaam__c = formData.achternaam;
bezoeker.E_mail__c = formData.email;
bezoeker.Evenement_item__c = formData.item;
// Waarden buiten de formData-collectie
bezoeker.drupalId__c = drupalId;
insert bezoeker;
System.debug('bezoeker toegevoegd '+bezoeker);
}
}
@RestResource annotatie
Laten we eens bovenaan beginnen.
@RestResource(urlMapping=’/getDataFromOurSystem/*’)
Met deze regel wordt aangegeven via welke URL de POST getriggerd wordt. Of zoals de developer guide van Salesforce mooi omschrijft: “to expose an Apex class as a REST resource“. De urlMapping staat op ‘/getDataFromOurSystem/*’ wat betekent dat de service reageert op verzoeken die “getDataFromOurSystem” in de URL hebben staan. De * is een wildcard, waarbij het voor de trigger niet uitmaakt wat er verder nog achter de URL staat. Zodra “getDataFromOurSystem” dus getriggerd wordt, onafhankelijk wat er nog meer achter staat, reageert de service. Bij de POST in Workbench gaan we dus deze URL gebruiken als patroon:
/services/apexrest/getDataFromOurSystem
Bij Postman zou voor deze URL de instance van Salesforce nog moeten staan dus:
https://voorbeeld.salesforce.com/services/apexrest/getDataFromOurSystem
Om deze annotatie te gebruiken, moet de APEX class altijd global zijn. Verder draait een webservice in systeemcontext, maar ik heb toch de “with sharing” aan de class toegevoegd dat helpt voorkomen dat onbedoeld gevoelige gegevens worden blootgesteld aan gebruikers die geen toegang zouden moeten hebben. Daarbij is het best practice om dit standaard te doen en houdt het de code consistent! We hebben nu:
@RestResource(urlMapping='/getDataFromOurSystem/*')
global with sharing class MyRestResource {
}
@HttpPost annotatie
We slaan even een stukje over (FormData class) en gaan door met de @HttpPost annotatie.
De @HttpPost annotatie in Apex maakt het eenvoudig om een methode te definiëren die kan worden aangeroepen via een HTTP POST-verzoek. Zo komt de @HttpPost boven de methode te staan waar het impact op heeft. In dit geval dus de doPost methode:
@HttpPost
global static void doPost () {
}
De methode moet global static
zijn. De global
zorgt ervoor dat de methode aangeroepen kan worden door de externe client die het HTTP POST-verzoek doet (wat noodzakelijk is in dit geval) en door static
toe te voegen hoef je geen instantie van de class aan te maken en kun je de methode direct aanroepen zonder extra overhead. De void
geeft aan dat de methode geen waarde retourneert wat in ons geval ook niet nodig is. Het algemene codeframe staat nu klaar:
@RestResource(urlMapping='/getDataFromOurSystem/*')
global with sharing class MyRestResource {
@HttpPost
global static void doPost () {
}
}
HTTP-verzoek ophalen
Dan gaan we het HTTP-verzoek zelf ophalen (de data) en vervolgens ermee werken.
Stap 1: het huidige HTTP-verzoek ophalen met RestRequest req = RestContext.request;
Stap 2: De body van het verzoek opslaan in een Blob met Blob body = req.requestBody;
Stap 3: Converteer de Blob naar een String met String requestBody = body.toString();
. Dit is handig omdat de meeste bewerkingen en verwerkingen van tekstgegevens in Apex met strings worden gedaan. In ons geval worden de JSON-gegevens dus naar een String omgezet. Nu moet deze String nog gedeserialiseerd worden naar een Map
zodat je ermee kunt werken in je Apex-code.
Map Structuur
Nu gaan we de JSON String omzetten naar een Apex Map object met sleutel-waardeparen. Het genest JSON-object wordt omgezet naar een List:
Map<String, Object> requestData = (Map<String, Object>)JSON.deserializeUntyped(requestBody);
Er wordt in deze een nieuwe variabele aangemaakt requestData
met een typecasting van <String, Object>, omdat we het object zo willen interpreteren. Nu kunnen we waarden ophalen op het hoogste niveau van de JSON. In dit geval is dat maar één sleutelpaar:String drupalId = (String)requestData.get('drupalId');
Zo wordt gezocht naar de key ‘drupalId’ en wordt de waarde daarvan opgeslagen in de variabele drupalId. Alle overige key/values zitten in Formdata. Nu moeten we het geneste JSON-object benaderen. Dat doen we door hier wederom een Map object van te maken:
Map<String, Object> formDataMap = (Map<String, Object>)requestData.get('formData');
Er wordt dus gezocht naar de key ‘formData’ en van de value wordt een los Map object gemaakt, zodat we deze kunnen benaderen. We kunnen nu gegevens direct uit het Map-object halen, maar om de gegevens op een gestructureerde en semantische manier op te halen, maken we een nieuw object aan.
FormData formData = new FormData();
Als we de klasse buiten de methode willen benaderen (misschien in de toekomst) moet deze worden gedefinieerd buiten de methode om.
public class FormData {
public String name;
public String achternaam;
public String email;
public String item;
}
En nu kunnen we ook de values omzetten naar variabelen van het formData object.
formData.name = (String)formDataMap.get('name');
formData.achternaam = (String)formDataMap.get('achternaam');
formData.email = (String)formDataMap.get('email');
formData.item = (String)formDataMap.get('item');
We zijn er bijna 🙂 Nu maken we een instantie van het SObject “Evenement_bezoeker__c” aan met:Evenement_bezoeker__c bezoeker = new Evenement_bezoeker__c();
En vervolgens voegen we de variabelen die we hierboven opgesteld hebben aan het object toe
bezoeker.Achternaam__c = formData.achternaam;
bezoeker.E_mail__c = formData.email;
bezoeker.Evenement_item__c = formData.item;
// Waarden buiten de formData-collectie
bezoeker.drupalId__c = drupalId;
De laatste stap, insert bezoeker;
, voert een DML (Data Manipulation Language) operatie uit om het record dat is vertegenwoordigd door de variabele bezoeker
toe te voegen aan de Salesforce-database. Uiteraard voegen we nog een System.debug uit, zodat we in de logs kunnen kijken of alles goed gaat. Als we vanuit Salesforce Workbench het verzoek indienen, zien we dat de gegevens succesvol in Salesforce gepusht worden.
Conclusie
Nu kan er dus JSON data verstuurd worden naar Salesforce toe via een bepaalde URL met HTTP POST in de REST API. Deze URL wordt vervolgens opgevangen, de data wordt omgezet naar een werkbaar format en daarna wordt er een nieuwe, in ons geval evenement bezoeker, aangemaakt in Salesforce op basis van deze JSON data.