Building custom REST API methods using CRMScript

When doing programming for SuperOffice you'll encounter a plethora of API's that you can use. There is the old COM-based API that only works with the Windows client, you've got the NetServer Webservices which are extensively used by the product itself, but is somewhat .NET focused, there is the SOAP based API that Ejournal had for working with Requests, and there is the new REST API which is recently released.

And then there’s the custom approach described in this blogpost.

Sometimes you just need to be able to supply a simple, highly specified, REST API endpoint for doing a specific task. This is typically going to be used by someone outside of the SuperOffice bubble that we live in. Perhaps the webguy responsible for the customers corporate website just needs an endpoint that he can POST to when someone fills out a form on his webpage. Or maybe the phone system needs to be able to retrieve a list of persons and their mobilenumbers.

Using CRMScript it is fairly trivial to write a script that can be called from the external system, and will let them get data from SuperOffice, or supply data to SuperOffice.

Here is an absolutely minimalistic example which defines a webservice endpoint for changing the name of a company.

%EJSCRIPT_START%
<%
Integer contactId = getCgiVariable("contactId").toInteger();
String name = getCgiVariable("name");

NSContactAgent contactAgent;
NSContactEntity contact = contactAgent.GetContactEntity(contactId);
contact.SetName(name);
contact = contactAgent.SaveContactEntity(contact);

%>
%EJSCRIPT_END%

This endpoint can be called by doing a GET to this URL (either directly in your browser, or by using a tool like Postman:

http://service.example.com/scripts/customer.exe?action=safeParse&includeId=something&key=abc&contact­Id=123&name=NewName

Now, obviously, this simple script has some room for improvement, but as a start I just wanted to show the minimum needed to do something.

Ok, lets improve the script by only allowing POST, validate the data passed in, and return some sort of statusindicator back to the caller.

%EJSCRIPT_START%
<%
Integer contactId = getCgiVariable("contactId").toInteger();
String name = getCgiVariable("name");

//Require POST
if (!cgiWasPost())
  return; 

//Validate input
if (contactId <= 0 || name.isEmpty())
  return;

NSContactAgent contactAgent;
NSContactEntity contact = contactAgent.GetContactEntity(contactId);
contact.SetName(name);
contact = contactAgent.SaveContactEntity(contact);

printLine("Ok, contactId " + contactId.toString() + " now has the name " + contact.GetName());

%>
%EJSCRIPT_END%

A couple of things you need to be aware of here. The cgiWasPost() is somewhat sensitive as to the Content-Type, so it has to be set to «x-www-form-urlencoded».

Also, at the end of the script we’re just printing out the response. Since we haven’t said otherwise this will be returned as Content-type «text/HTML» and with HTTP status code «200 OK».

Let’s change the example a bit so that it can be used to retrieve data from SuperOffice as JSON, and show how we also can control the HTTP Status Code returned.

%EJSCRIPT_START%
<%
#setLanguageLevel 3;

//https://sod.superoffice.com/Custxxxxx/CS/scripts/customer.fcgi?action=safeParse&includeId=simple&k­ey=abcde

//Set HTTP headers
Map _headers;
_headers.insert("Content-Type", "application/json;charset=utf-8");
_headers.insert("X-Custom-Header", "abc");

//Grab incoming CGI parameters
Map result;
Integer personId = getCgiVariable("personId").toInteger();
if (personId == 0)
  _headers.insert("Status", "406 Bad Request - No personId found");
else {
  //Lookup data in SuperOffice
  NSPersonAgent personAgent;
  NSPerson person = personAgent.GetPerson(personId);
  if (person.GetPersonId() != personId)
  {
    _headers.insert("Status", "404 Person not found");
  }
  else {
    result.insert("firstname", person.GetFirstname());
    result.insert("lastname", person.GetLastname());
    _headers.insert("Status", "200 OK");
  }
}

//Return headers and data
String h;
for (_headers.first(); !_headers.eof(); _headers.next())
	h += _headers.getKey() + ": " + _headers.getVal() + "\n";

setParserVariable("ej.headers", h);
printLine(result.toJson());

%>
%EJSCRIPT_END%

Here there is more stuff going on. We’re setting the HTTP headers manually, so we have total control of all the bits returned to the caller. We’ve got full access to setting things like Content-Types, HTTP status codes and custom X-headers.

Hopefully, you get the sense that this is extremely powerful, and the possibilities here are endless. Typically these kind of custom endpoints are used for providing a way to create new tickets, update existing companies or extract a list project or sales.

The examples above are fairly trivial, and a real world script can end up being quite a bit longer than the examples. A pro-tip is to split up the scripts, so that the entry-script, which has the %EJSCRIPT_START% tags will include other scripts. Those other scripts does not need %EJSCRIPT% tags. By splitting it up you’ll get a better developer experience, since the syntax checking performed when saving a script doesn’t work for %EJSCRIPT%-type of scripts.

With great power comes great responsibility

Execution of these types of endpoints are done as a system user, so you could end up getting data which is private. The URL to a script, if shared inappropriately, could give someone access to data they shouldn’t have access to. I recommend also putting some work into building an authentication mechanism for these scripts, like some sort of token or username/password based criterias which could be revoked. Also, logging incoming requests and the responses can be benifitial.

These scripts work both for Online and Onsite environments, but in Online there are a few extra considerations. A script is not allowed to run for more than 60 seconds, and it cannot consume more than 8MB of RAM. If the scripts breaks any of those two rules it’ll be killed. Furthermore, you must make sure you do not call your script too frequently. If your script is executed too often or creates too much load, it will be throttled.

So, there you have it. If the standard API’s supplied by SuperOffice doesn’t cut it for you, you’ve now got one more option you can use.

Post Comment