[SalesForce] Access MetadataAPI via JavaScript

We created an 100% native Salesforce IDE as a Managed Package which is using ToolingAPI and MetadataAPI via APEX. This works fine. But there is one thing which really bothers people after the installation of the package: they are annoyed be the well know endpoint-exception:

System.CalloutException: IO Exception: Unauthorized endpoint, please check Setup->Security->Remote site settings

In my opinion it is very bad to let users run into that exception – even if you may assume that developers are the target-group and they will figure the simple steps to proceed. We want to do it better.

What we have done first, was to include the most common endpoints as Remote-Sites in the packaged – knowing that we'll never be able to cover all due to the myDomain feature. So we have done it for na0..naX, eu0..euX and csX. Now, I have developed a strong dislike to this kind of bulk-inclusion, because:

  • it is bad to include > 30 endpoints in a (frustrated) preemptive measure when only a single one is really required.
  • there is an issue in the installer, when you need to confirm the endpoints and the list is too long, the confirm-button falls out of the visible screen area…
  • for sure, security review will dislike that sort of misdemeanor – and I totally agree.

So what can we do? Writing an manual or display a nice list of post-install manual steps? C'mon, still not nice. One guy from Salesforce suggested a Heroku-Proxy: APEX calls Heroku (one static endpoint, passing required endpoint as parameter) and Heroku calls back MetadataAPI to configure the endpoint… hmmmm… overkill…? What else?

I just read this very cool answer from @sfdcfox here Dynamically set remote site Setting in Apex
Fast-forward discarding the options Flash, Java, Silverlight and ThirdParty as having too many tradeoffs, I really love the JavaScript idea!

Now, my draft would be to create a Post-Install page which contains the JavaScript. First thing to ask for username, password and token (or better go with OAuth?). Then to access MetadataAPI via a callout by Javascript and dynamically add the single endpoint to the org. Bottom-line: the Heroku-Pattern without Heroku 😉

And finally here is my question: does anyone of you have accessed MetadataAPI directly from JavaScript? Even if in a completely different use-case, a JS-callout-skeleton to do just "something" with the MetadataAPI would help me a lot to figure out the final solution on my own. Or are there any obvious show-stoppers which prevent JavaScript from accessing the MetadateAPI?

I think it could be worth to share a concept here, since it would be a repeatable pattern usable for many use cases.

Best Answer

You can actually call the metadata API from JavaScript exceptionally easy. I threw this together in response to this question:

var binding = new XMLHttpRequest(), payload = '<?xml version="1.0" encoding="utf-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-Instance"> <soapenv:Header> <urn:SessionHeader xmlns:urn="http://soap.sforce.com/2006/04/metadata"> <urn:sessionId>{!$Api.Session_ID}</urn:sessionId> </urn:SessionHeader> </soapenv:Header> <soapenv:Body> <createMetadata xmlns="http://soap.sforce.com/2006/04/metadata"> <metadata xsi:type="ns2:RemoteSiteSetting" xmlns:ns2="http://soap.sforce.com/2006/04/metadata"> <fullName>This Server</fullName> <description>The API endpoint</description> <disableProtocolSecurity>false</disableProtocolSecurity> <isActive>true</isActive> <url>{0}</url> </metadata> </createMetadata> </soapenv:Body></soapenv:Envelope>'.replace('{0}',window.top.location.protocol+'//'+window.top.location.hostname+'/');
    binding.open('POST', window.top.location.protocol+'//'+window.top.location.hostname+'/services/Soap/m/31.0');
    binding.setRequestHeader('SOAPAction','""');
    binding.setRequestHeader('Content-Type', 'text/xml');
    binding.onreadystatechange = function() { if(this.readyState==4) handleResponse(); }
    binding.send(payload);

Everything appears to work (I was getting some odd Invalid Session ID when I tried to grab it from the cookie directly, so the merge field should fix that). Feedback welcome. Also, you'll want to add the "base" version as well (use getSalesforceBaseUrl() in Apex Code to discover the correct URL to use).

Related Topic