Considerations when implementing JSSP projects

General JavaScript programming considerations

  • The JSSP uses the Microsoft Chakra engine for JavaScript execution. See About the Chakra Engine for details about the engine.
  • You can use awaits, async, and promises in your code. See the sample projects for examples of using these executions in JavaScript.
  • You cannot use any polyfills that target Node.JS. For example, use fetch and not node-fetch. If you don't have access to a polyfill, you can try to create one from a node.js package using a tool like browserify, but this requires extensive testing to ensure JSSP supports the types of browser calls.
  • You will not be able to directly use Node.js modules since the Chakra engine does not run on the V8 engine. If you need to use a Node.js module, see if you can use a polyfill approach or use browserify to bundle dependencies.
  • JSSP uses XMLHttpRequest (XHR) Requests to interact with third-party APIs. See Working with XHR Requests for details about using XHR
  • Avoid doing computationally expensive work in your JS code, as K2 Cloud Operations monitors and limits resource usage to ensure system reliability. By default there is a 5-second timeout for JS script computation. Essentially, you need to limit any computation done in your JS code for any one service method operation to less than 5 seconds. Note that the timeout excludes the intervals to send and receive XHR requests, and therefore also excludes long-running operations such as file uploads/downloads.
  • Do not use namespaced values for properties and methods; this results in errors when associating generated SmartObjects.
    Copy

    Example of flatting a Twitter response by using dots for the payload and concatenating likes

    function onExecuteSomeMethod(properties) {
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if (xhr.readyState !== 3 || xhr.status !== 200) return;
            const data = JSON.parse(xhr.responseText);
            let likes = 0;
            for (var i = 0; i & lt; response.data.posts.length; i++) {
                likes += data.posts[i].likes;
            }
            postResult({
                'fullName': data.name.first + response.data.name.second,
                'firstName': data.name.first,
                'lastName': data.name.last,
                'address1': data.address.lines[0],
                'address2': data.address.lines[1],
                'addressCity': data.address.city,
                'country': data.address.country,
                'zip': data.address.zip,
                'contact': data.email || response.data.twitter,
                likes
            });
        }
        xhr.open("GET", "https://example.com/users" + encodeURIComponent(properties["username"]));
        xhr.send();
    }
  • Console output methods only work if you pass in a parameter. For example, console.count() won't write anything to console output, but console.count(foo) will if foo is previously defined. This applies to console methods such as console.count(), console.profile(), console.time(), console.assert(false), .console.error(), and console.exception(). See Troubleshooting and Debugging for more information on testing and console logging with JSSP.
  • The PostResult function accepts arrays, so you can use object array mapping as demonstrated in this component: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map and in the sample projects. You do not have to use for loops for list items.
  • For protection, you should always encode any parameters that are passed to your SmartObject methods using the encodeURIComponent method. For example, in the following snippet, encoding is done only on the parameter that is passed to the method. You might be tempted to encode the entire component string, but that can result in badly-formed requests. If you think you have a bad request, log a message to the console that includes your URL, and then use that URL in a tool like Postman to troubleshoot what might be happening.
    Copy

    Using the encodeURIComponent method

    let component = "?$filter=startswith(displayName, '" + encodeURIComponent(parameters[TeamDisplayNameStartsWith]) + "')&$select=id,displayName,resourceProvisioningOptions";
    var url = baseUriEndpoint + "/groups" + component;

Working with XMLHTTP (XHR) Requests

XMLHttpRequest is how you make internet outbound requests to interact with 3rd party APIs. The complete JS XMLHttpRequest specification is located at XMLHttpRequest standard. Review the bullets below for specific considerations and notes about the use of XHR in JSSP.

  • You can only make XHR request to the outbound internet and cannot access services on the local network
  • No timeout setting is currently available for the XHR request
  • You should not use synchronous i/o on your XHR requests. JSSP does not support this
  • new XMLHttpRequest() is the constructor
  • readyState: see https://xhr.spec.whatwg.org/#states
  • error: Allows you to get the error that has been thrown during an XHR request
  • open(method, url):
    • Opens the XHR request that must point to the outbound internet.
    • Supported methods: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
    • Unsupported methods: CONNECT, TRACE, TRACK
    • Async argument is assumed to always be true
    • Username and password parameters for the Open() overload are not implemented. You can use the Static Authentication mode for the Service Instance and the product will pass the username and password specified in the Service Instance configuration.
    • Not handling the case where a fetch can be ongoing
  • send(body):
    • Sends the request through the network calling the 3rd party API.
    • extractedContentType is not implemented currently. Content-Type can be set via a header
    • Synchronous flag is not completely implemented, as the async operations per spec requires tracking every time a chunk is read from the response stream and updating counters accordingly. Currently it behaves more like the "synchronous flag" operation, but handled error handling and callbacks in the async pattern
    • Timeout flag is not implemented
    • Load/Loadend events with transmitted and length parameters are not set
  • setrequestheader():
    • Adds a header to the request
    • Forbidden headers: ACCEPT-CHARSET, ACCEPT-ENCODING, ACCESS-CONTROL-REQUEST-HEADERS, ACCESS-CONTROL-REQUEST-METHOD, CONNECTION, CONTENT-LENGTH, COOKIE, COOKIE2, DATE, DNT, EXPECT, HOST, KEEP-ALIVE, ORIGIN, REFERER, TE, TRAILER, TRANSFER-ENCODING, UPGRADE, VIA
    • Not handling combining Name/Value in the author request headers
  • withCredentials
    • Boolean attribute indicating that the request should use either Basic or OAuth authorization. If set to true, the default Authorization header will be passed-through from the product out to the 3rd party service via the header that is set on the Service Instance configuration. By default the Authorization Header Format is set to Bearer {0}, where {0} is the format specifier where the token will be added and Authorization Header Name is used to override which header is used for auth.
  • response
    • getResponseHeader(name): Gets a header that is contained in the response
    • status: The HTTP status code of the response.
    • responseText: The raw text returned from the response.
    • response: the content returned from the response. Can be a string, JSON or the BLOB content (used for File & Image).
    • responseType
      • If the response type is set to "json" it will parse the response as JSON.
      • If the response type is set to "text" it will be parsed as a string.
      • .If the response type is set to "blob" the content will be treated as a blob.
      • Does not check for xml MIME type, see point 3 in the "text" response section of https://xhr.spec.whatwg.org/#response-body
      • Does not do decoding on received bytes for different text encoding types, see points 4 and 5 in https://xhr.spec.whatwg.org/#response-body.
      • As we don't have the possibility of the current global object being a window, these checks are not implemented, see point 1 and 3 in https://xhr.spec.whatwg.org/#response-body
    • responseURL; The final URL of the response including redirects
  • events
    • onreadystatechange: An event callback used whenever the readyState attribute changes.
    • onerror: An event callback when an error on the network occurs.
  • FormData: The FormData object is used for File upload scenarios within JSSP
    • Supported methods inlucde new FormData(), append(name, value) and append(name, value, filename)

Data Types considerations

  • Review the list of Data Types to see what data types are supported by Service Objects. You cannot define custom data types in the Service Object definition.
  • Working with Number types
    • JS doesn't support int64 (only doubles), but JSSP will parse a 64-bit integer string to a int64 to pass back to the product.
    • NaN and Infinity return 0 for number data types. Undefined returns null.
  • Working with Dates:
    • When you need to handle dates in your JavaScript, make sure you understand how the Date object in JavaScript works and how it may differ from C# and product dates. If you want to test how a date is handled, open the developer tools in your browser (F12) and go to the console. In the console you can type in date functions with various strings to see how JavaScript is handling a particular date or some date-based math functions.
    • Dates that return Not A Number (NAN) in JavaScript are handled as NULL values in the product. Keep this in mind if you see null dates where you expect to see a date value.
    • Date types assume the culture of the server where the JSSP Service is running.
    • You must pass back a Date() object on postResult, or a double that represents the time, such as Date.parse("").
    • Using 0001-01-01 in a timezone that is negative will cause an exception.
    • Use a Date object in your JavaScript code. Do not try to manage date parsing and handling on your own.
  • Working with Files and Attachments
    • When accessing a parameter/property that is of type file or image, the size property will always be -1 and the type will be application/octet-stream, e.g. properties["file1"].content.size and properties["file1"].content.type
    • While you can define Service Object method parameters of type image/attachment, the product UI may not support these types as parameters. Rather pass attachment/image types as Service Object Method Properties.
    • You can use simple file upload, session file upload, and multi-part form data file upload, depending on the service you're connecting to. See Uploading Files for examples of how to do each type of file upload.
    • When working with file names, especially when using Multi-part Form Data, the service you are calling may expect quotes around all file names (even names without spaces). Most modern services do not require quotes around the file name. If you see errors in JSSP, test files with spaces in the file name, and files without spaces in the filename. The product does not automatically add quotes unless there is a space in the file name; try adding quotes in your broker code to determine if the service needs all file names in quotes.
    • When uploading files using Form Data, an additional parameter (filename*) is added to the header per RFC 6266. The final header would look similar to the following:
      Content-Disposition: form-data; name=file; filename=attachment.bin; filename*=utf-8''attachment.bin

Uploading Files

If you need to upload files, use the following code snippets as a starting point.

Copy

Simple File Upload

//sample simple file upload function using DropBox API
//https://www.dropbox.com/developers/documentation/http/documentation#files-upload
function executeUploadFileMethod(parameters, properties) {
    var path = "/Folder1/" + properties["file1"].filename;
    var accessToken = "eI5X8aj8zHAAAAAAAAAKcuhoKD3hJt6l8bX1htoyST9dtvGVuj_WxbdkMtEfrQHF";
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState !== 4) return;
        if (xhr.status !== 200) throw new Error(xhr.response);
        postResult({
            "result": "File uploaded successfully"
        });
    };
    xhr.open("POST", 'https://content.dropboxapi.com/2/files/upload');
    xhr.setRequestHeader("Authorization", "Bearer " + accessToken);
    xhr.setRequestHeader("Content-Type", "application/octet-stream");
    xhr.setRequestHeader("Dropbox-API-Arg", '{"path": "' + path + '"}');
    xhr.send(properties["file1"]);
}
Copy

Session File Upload

//sample session file upload function using Google Drive API
//https://developers.google.com/drive/api/v3/manage-uploads
function executeUploadFileMethod(parameters, properties) {
    var metadata = {
        "name": properties["file1"].filename
    };
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState !== 4) return;
        if (xhr.status !== 200) throw new Error(xhr.response);
        var uploadUrl = xhr.getResponseHeader("Location");
        var xhr2 = new XMLHttpRequest();
        xhr2.onreadystatechange = function () {
            if (xhr2.readyState !== 4) return;
            if (xhr2.status !== 200) throw new Error(xhr2.response);
            postResult({
                "result": "File uploaded successfully"
            });
        };
        xhr2.open("PUT", uploadUrl);
        xhr2.responseType = "json";
        xhr2.setRequestHeader("Content-Type", "text/plain");
        xhr2.setRequestHeader("Authorization", "Bearer ya29.a0Adw1xeXn6e1pyCzcOOtHxRvRgVmqpJxiv2H-hrCn1ZlKdP1eAUp-64WAncgNgGyeupzs9EP7NyRp0onfiVgaubbZUKqlFjDfgNON_Hj1CRJVwD7n8B2aU4O7PpBjByY2SunthMKO0nnYSD1iMhC5RXgx_3ypSn_Tnus");
        xhr2.send(properties["file1"]);
    };
    xhr.open("POST", 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable');
    xhr.responseType = "json";
    xhr.setRequestHeader("X-Upload-Content-Type", "text/plain");
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.setRequestHeader("Authorization", "Bearer ya29.a0Adw1xeXn6e1pyCzcOOtHxRvRgVmqpJxiv2H-hrCn1ZlKdP1eAUp-64WAncgNgGyeupzs9EP7NyRp0onfiVgaubbZUKqlFjDfgNON_Hj1CRJVwD7n8B2aU4O7PpBjByY2SunthMKO0nnYSD1iMhC5RXgx_3ypSn_Tnus");
    xhr.send(JSON.stringify(metadata));
}
Copy

Multi-part Form Data File Upload

//sample Multi-part Form Data file upload function using Box API
function executeUploadFileMethod(parameters, properties) {
    var oauthToken = "o954JTWP4IFkdSEIQbK2uIhH558i58XB";
    var form = new FormData();
    form.append('attributes', JSON.stringify({
        "name": properties["file1"].filename,
        "parent": {
            "id": "0"
        }
    })); //IMPORTANT
    form.append('file', properties["file1"].content);
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState !== 4) return;
        if (xhr.status !== 201) throw new Error("Failed with status " + JSON.stringify(xhr.response));
        postResult({
            "result": "File uploaded successfully" + JSON.stringify(xhr.response)
        });
    };
    xhr.open("POST", 'https://upload.box.com/api/2.0/files/content', true);
    xhr.setRequestHeader('Authorization', 'Bearer ' + oauthToken);
    xhr.send(form);
}

About the Chakra Engine

The JSSP uses the Microsoft Chakra engine for JavaScript execution, which runs as a service in the environment. It is important to know that for the engine to do anything, that you must enable it with the appropriate interfaces.

The Chakra core is enabled only for XHR to make web requests, communicate with the SmartObject APIs, use cached OAuth tokens, and log console methods. If you try to include dependencies that rely on other common, browser-based functionality such as local disk, temporary storage, or network access, those methods will fail. The engine for the JSSP includes a small subset of what JavaScript developers can use in the context of a web browser, so keep that in mind as you're planning your JS broker projects

There are many technical and business reasons why the product uses the Microsoft Chakra engine and not the Google V8 engine. These reasons are not covered in this section but know that the product has a mechanism to update the engine when Microsoft releases updates and will respond within the appropriate timeframe given the nature of the updates (tuning, bug fixing, security vulnerability fix, etc.).

The Chakra engine used in the product does not have the same functionality as a JavaScript engine in a web browser or the Google V8 engine. You will not be able to directly use Node.js modules since the Chakra engine does not run on the V8 engine. If you need to use a Node.js module, see if you can use a polyfill approach or use browserify to bundle dependencies.