RPC and REST

As you probably know, scriptr.io’s scripts are the main component used for implementing business process and/or secure API for your IoT applications. Hence, any script you write in scriptr.io is immediately accessible to remote clients, through HTTP or Web Sockets.

RPC

If you have been browsing through the different posts in this blog, you might have noticed that in most of our examples, we actually adopt the RPC (Remote Procedure Call) style when implementing sample APIs. As it’s name implies, RPC is about invoking a remote function (action), therefore, scripts used to implement RPC-based API usually have the name of the function (action) they execute on one or many server-side resources. In the below example, the name of the script indicates that the script returns a list of alerts:

// Endpoint https://api.scriptrapps.io/listAlerts
// Example of a possible implementation of listAlerts
var document = require(document);
var resp = document.query({query:'type="alert"'});
if (resp.metadata.status == "success") {
  return resp.result.documents;
}
return resp;

While RPC-based operations/APIs leverage HTTP to invoke remote operations and are widely used in SOA, they are sometimes erroneously referred to as “REST” operations/APIs.

REST

At the risk of simplifying a bit, one can say that REST is an approach that relies on HTTP methods – notably GET, POST, PUT, DELETE (and PATCH), to manipulate remote resources. Indeed, most operations in distributed applications consist in creating, updating, reading an deleting resources, which are operations that can easily be mapped to the POST, PUT, GET and DELETE methods, as illustrated in the examples hereafter:

  • curl -X GET http://apis.scriptrapps.io/alerts : lists alerts
  • curl -X GET http://apis.scriptrapps.io/alerts/1234567 : get alert with id 1234567
  • curl -X POST http://apis.scriptrapps.io/alerts : create a new alert and use form data to initialize it
    -H ‘content-type: multipart/form-data;
    -F id=1234567
    -F ‘alertType=low signal’
    -F ‘device=sim AT&T’
    -F ‘status’=raised’
  • curl -X PUT http://apis.scriptrapps.io/alerts : update the status property of an existing alert
    -H ‘content-type: multipart/form-data
    -F ‘id=1234567’
    -F ‘status=closed’
  • curl -X DELETE http://apis.scriptrapps.io/alerts/1234567 : delete existing alert with id 1234567

REST is an interesting approach, notably because it is straightforward and simple to adopt. So let’s see how to implement REST-based APIs with scriptr.io.

Implementing REST-based APIs with scriptr.io

It is rather simple to turn scripts into RESTful services. What we need is the following:

  • Create a resource type,
  • Create a script named after the resource type,
  • In the script, obtain the identifier of a specific resource from the URL or the request,
  • In the script, determine the HTTP method that was used to invoke the script,
  • In the script, retrieve the request parameters (from the request’s body or query string),
  • In the script, trigger create, read, update and delete (CRUD) operations on the targeted resources, depending on the HTTP verb.

Create a resource type

Since our resources must be persistent, we will leverage scriptr.io’s schemas to create a new document type (the schema). Creating a schema is very easy: from your scriptr.io workspace, open the Data Explorer. From the latter, click on “Schema” then “New” to create a new schema.

For the sake of our example, let’s assume we need to persist SIM cards signal quality data (SINR, RSSI and bit rate). We’ll thus create the following schema:

<schema>
	<aclGroups>
	    <!-- 
	      this section specifies read-write permission on fields
	      and the users, groups and roles who have these permissions
	    -->
		<aclGroup name='authenticated-users'>
			<read>authenticated</read>
			<write>authenticated</write>
			<fields>
				<field>sinr</field>
				<field>rssi</field>
				<field>bitrate</field>
			</fields>
		</aclGroup>
		<schemaAcl>
			<read>nobody</read>
			<write>nobody</write>
			<delete>nobody</delete>
		</schemaAcl>
	</aclGroups>
	<fields>
		<field name='sinr' type='numeric'>
			<validation>
				<cardinality min='1' max='1'/>
			</validation>
		</field>
		<field name='rssi' type='numeric'>
			<validation>
				<cardinality min='1' max='1'/>
			</validation>
		</field>
		<field name='bitrate' type='numeric'>
			<validation>
				<cardinality min='1' max='1'/>
			</validation>
		</field>
	</fields>
</schema>

If you need to allow authenticated users to write to/delete documents that are bound to a schema, you also have to configure your scriptr.io store to authorize this. From the Data Explorer, click on “Store” > “DefaultStore” (or the store you’re targeting) > “Edit Store” then set the values of “SaveDocumentACL” and “DeleteDocumentACL” to “authenticated”.

Now that we have defined the “sim” schema, we can start saving/querying documents of this type using the native “document” module, e.g.

var document = require("document");
// create a new document of type "sim" (notice the "meta.schema" field)
document.create({"rssi": -71, "sinr": 12, "bitrate": 126, "meta.schema": "sim"});
// list all documents of type "sim"
var queryExpression = 'rssi &lt; -70 and apsdb.schema = "sim" ';
var resp = document.query({"query": queryExpression, "fields": "sinr, rssi, bitrate'});

Obtain the identifier of a specific resource from the URL or the request

This is done using the “pathInfo” property of “request”, which is automatically populated when a request is issued to a script, with the part of the request’s URL starting after the name of the script.

// Example: script's URL: https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim
// script content:
return {"pathInfo": request.pathInfo}

Sending the following request to the script:

curl -X GET https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim/123456789 

Produces the following output:

{"response": {
  "metadata": {
    "requestId": "c2b18a91-846b-4dc4-8165-b5d1ae6fbc09",
    "status": "success",
    "statusCode": "200"
  },"result": {
 "pathInfo": "123456789"

Determine the HTTP method that was used to invoke the script

We use the “method” property of The request object, which contains the HTTP method used in the request sent to the script.

// Example: script's URL: https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim
// script content:
return {"method": request.method}

Sending the following request to the script:

curl -X GET https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim/123456789 

Produces the following output

{"response": {
  "metadata": {
    "requestId": "c2b18a91-846b-4dc4-8165-b5d1ae6fbc09",
    "status": "success",
    "statusCode": "200"
  },"result": {
 "method": "GET"

Retrieve the request parameters

Depending on the content-type specified in the request, the request’s parameters will either be available in the “parameters” property of the “request” object, or in it’s “body” or “rawBody” properties. Whenever parameters are sent as part of the query string, or if the request’s content-type is “application/x-www-form-urlencoded” or “application/form-data”, the request’s parameters is automatically parsed by scriptr.io into request.parameters. If the request’s content-type is applicaton/json, then the request’s body is parsed into request.body. Last, if scriptr.io is unable to parse the request’s parameters, or if the request’s content type is one of “application/xml” or “text/plain”, the whatever is sent in the body of the HTTP request is available in request.rawBody;

// script's URL: https://iotdemos.scriptrapps.io/blog/rest_tutorial/sims
// script content:
return {  
    "body": request.body,
    "parameters": request.parameters
};

Sending the following request to the script:

curl -X PUT https://iotdemos.scriptrapps.io/blog/rest_tutorial/sims/123456789 
   -H 'content-type: application/json'
   -d '{
	"sinr":12.4,
	"rssi":-70,
	"bitrate":256
    }'

Produces the below output:

{"response": {
  "metadata": {
    "requestId": "c2b18a91-846b-4dc4-8165-b5d1ae6fbc09",
    "status": "success",
    "statusCode": "200"
  },"result": {
 "body": {
  "sinr": 12.4,
  "rssi": -70,
  "bitrate": 256
 },
 "parameters": {}
}
}}

Trigger create, read, update and delete (CRUD) operations on the targeted resources

Using the parameters sent to the script and the HTTP method, it is easy to use a switch/case statement to execute the appropriate behavior:

var method = request.method;
switch(method.toLowerCase()) {
   
    case "get": {
        if (resourceId) { 
            return readSim(resourceId);
        }else {
            return listSims(content);
        }
    };break;
    case "post": return createSim(resourceId, content);
    case "put": return updateSim(resourceId, content);
    case "delete": return deleteSim(resourceId);
    default: return {errorCode: "Unsupported_Method", errorDetail: method + " is not supported by the API"};
};

The complete example

var document = require("document");

var method = request.method;
var resourceId = request.pathInfo; 

// copy all received parameters and body into the content varible
var content = {};
if (request.parameters) {
    
    for (var key in request.parameters){
        content[key] = request.parameters[key];
    }
}

if (request.body) {
    
    for (var key in request.body){
        content[key] = request.body[key]
    }
}

// Trigger CRUD operations depending on HTTP method
switch(method.toLowerCase()) {

    case "get": {
        if (resourceId) { 
            return readSim(resourceId);
        }else {
            return listSims(content);
        }
    };
    case "post": return createSim(resourceId, content);
    case "put": return updateSim(resourceId, content);
    case "delete": return deleteSim(resourceId);
    default: return {errorCode: "Unsupported_Method", errorDetail: method + " is not supported by the API"};
};

function readSim(resId) {
    return document.get(resId);
}

function listSims(content) {
    
    var resp = document.query({query: 'apsdb.schema = "sim"', fields: "*", count: true});
    if (content &amp;&amp; content.pageIndex) {
        resp.pageIndex = content.pageIndex;
    }
    
    if (resp.result) {
        return resp.result.documents;
    }
    
    return resp;
}

function createSim(resId, content) {

    var fields = content;
    fields.key = resId;   
    fields["meta.schema"] = "sim";
    var resp = document.create(fields); // document.create()returns an error if resId already exists
    if (resp.result) {
        return resp.result.document.key;
    }
    
    return resp;
}

function updateSim(resId, content) {

    var fields = content;
    fields.key = resId;
    var resp = document.save(fields); // document.save() creates the resource if resId does not exist
    if (resp.result) {
        return resp.result.document.key;
    }
    
    return resp;
}
function deleteSim(resId) {
    
    var resp = document.delete(resId);
    if (resp.result) {
      return resp.result;
    }
    
    return resp;
}

Try it

// !! Make sure to change the id at the end of the URL to avoid duplication !!
curl -X PUT https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim/345889 
  -H 'content-type: application/json' 
  -d '{
	"sinr":10.5,
	"rssi": -65,
	"bitrate": 256
   }'

curl -X GET https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim/345889

curl -X PUT https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim/345889  
  -H 'content-type: application/json' 
  -d '{
	"sinr":12.4,
	"rssi": -70,
	"bitrate": 256
   }'

curl -X GET https://iotdemos.scriptrapps.io/blog/rest_tutorial/sim