Understanding JavaScript Service Provider project structure

There are a couple of key components of a JavaScript Service Provider (JSSP) project you must understand before you can write your own. The files index.ts and package.json are the two code files that you will most likely spend most of your time, with index.ts being where you actually code the broker.

If you have set up your development environment as suggested, when you build your project with NPM a bundled .js file is generated in the /dist folder, this is the file you will expose in a URL and use to create a service type in K2.

index.ts

This is where you write most of your broker code. You can use any language, but if you use TypeScript you can get Intellisense by using the k2-broker-core library. You'll notice that all samples are written in TypeScript and have a similar project structure based on the template.

In your index.ts file, you must include three different sections: metadata, ondescribe, and onexecute.

metadata

In this section, you provide information about your service type and specify service keys. Service keys are typically per-instance configuration values, such as a connection URL.

The sample below illustrates a metadata section with two service configuration keys:

Copy

Metadata section with two service configuration keys

import '@k2oss/k2-broker-core';
metadata = {
 "systemName": "MyServiceBroker", //a string defining the system name
 "displayName": "My Service Broker", //a string defining the display name shown in UI
 "description": "My JavaScript service broker.",
//configuration values - an object defining the K2 Service Keys when registering a Service Instance  
"configuration": {
  "MyServiceKey1": {
   "displayName": "My Service Key 1",
   "type": "string"
  },
//service key with a default value
  "MyServiceKey2": {
   "displayName": "My Service Key 2", //the display name of the service key
   "type": "number", //the type of the service key. this type is ignored by K2 and will always be a string type.
//This can come in handy when you want to store the type information and use it later on in ondescribe or onexecute
   "required": "true", //boolean value indicating to K2 that this service key must be set
   "value": "1" //string value representing the default value used by K2
  }
 }
};

You can pass service keys to the OnDescribe and OnExecute methods as the last parameter, and then refer to the configuration value in the method code like this:

Copy

Passing service keys

var KeyValue = configuration["MyServiceKey1"];

ondescribe

This is the section that contains the definitions of your business objects. OnDescribe is called when you register/refresh a Service Instance and K2 discovers the available Service Objects in the Service Instance.

Describing objects statically is a simpler approach to generating business objects, where you write code to describe static object per system object. See the code sample below that illustrates how to statically define a service object, the properties of the service object, and then methods of the service object:

Copy

Statically define a service object

//sample that describes one object (Task) with one method (Get Task)
ondescribe = async function ({ configuration }): Promise<void> {
    postSchema({ //can only be used within the ondescribe function that is used by K2 to get the schema of the broker
        objects: { //an object property that contains all of the service object schemas
            "task": {
                displayName: "Task",
                description: "Manages a task list",
                properties: { //an object containing other objects for the service object properties
                    "id": {
                        displayName: "ID",
                        type: "number", //the K2 data type of this property
                        //extendedType: "extenedTypeName", //a string that corresponds to K2 extended types.
                        //This will only work if the type has been set to something that has an extended type
                        //data: {}, //an object that is saved and reserved for the JS developer to save additional data
                        //with the property to use upon execution with the schema object provided in the onexecute(obj) method signature
                    },
                    "title": {
                        displayName: "Title",
                        type: "string"
                    },
                    "completed": {
                        displayName: "Completed",
                        type: "boolean"
                    }
                },
                methods: { //an object that contains other objects to describe the schema of the service object methods
                    "get": {
                        displayName: "Get Task",
                        type: "read", //string corresponding to the K2 method types.
                        //Values can be read, list, execute, update, delete, and create
                        inputs: ["id"], //input properties for the method
                        outputs: ["id", "title", "completed"] //properties returned by the method
                    },
                    "getParams": {
                        displayName: "Get",
                        type: "read",
                        parameters: { //an object containing objects representing method parameters
                            "pid": {
                                displayName: "param1",
                                description: "Description Of Param 1",
                                type: "number" //the K2 data type of this parameter
                                //extendedType: "extenedTypeName", //a string that corresponds to K2 extended types.
                                //This will only work if the type has been set to something that has an extended type
                                //data: {}, //an object that is saved and reserved for the JS developer to save additional data
                                //with the property to use upon execution with the schema object provided in the onexecute(obj) method signature
                            }
                        },
                        requiredParameters: ["pid"], //array of strings referencing the system names of the
                        //method parameters required for execution
                        inputs: ["id"], //array of strings of system names of the properties that are available for input
                        //requiredInputs: ["id"] //array of strings of system names of the properties that are required for input
                        outputs: ["id"] //array of strings of system names of properties returned upon execution
                        //data: {}, //an object that is saved and reserved for the JS developer to save additional data
                    }
                }
            }
        }
    });
}

If you do not want to statically declare all your objects in the ondescribe section, you could build your objects dynamically by using postSchema to send the object structure to K2, as shown below:

Copy

Dynamically build your objects

ondescribe = async function ({ configuration }): Promise<void> {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState !== 4) return;
        if (xhr.status !== 200) throw new Error("Failed with status " + xhr.status);
        var obj = JSON.parse(xhr.responseText);
        postSchema({
            objects: {
                "object1": {
                    displayName: obj['result'],
                    description: obj['header']
                }
            }
        });
    };
    xhr.open("GET", 'https://myservice.com/service');
    xhr.withCredentials = 'true'
    xhr.send();
};

onexecute

This is the section that calls to the service and processes the JSON payload returned. Onexecute is called at runtime when a SmartObject based on a Service Object is executed. Typically, you would interrogate the input parameters in the onexecute method to determine which object and which method was called, and then direct processing accordingly to a helper method to execute the actual code. In the sample below, onexecute contains case statements to determine the object being called, and then in the object execution method, we determine which method was called and direct the code processing accordingly.

You can use PostMan to help generate the code you'll need to implement in the onexecute method.
Copy

onexecute section

onexecute = async function ({ objectName, methodName, parameters, properties, configuration, schema }): Promise<void> {
    //objectname - string value representing the name of the executing Service Object name
    //methodname - string value representing the executing Service Object method name
    //parameters - object containing the input parameters for the Service Object method
    //properties - object containing the inputs to the Service Object that is currently executing
    //configuration - an object containing the configuration (service keys) for the Service Instance
    //schema - an object containing the entire schema for the service type (defined in postSchema)

    //example: how to retrieve a configuration key value
    var KeyValue = configuration["MyServiceKey1"];

    //determine which service object we are executing
    switch (objectName) {
        case "object1":
            //call the execute method for object 1
            await onexecuteObject1(methodName, parameters, properties, configuration);
            break;
        case "object2":
            //call the execute method for object 2
            await onexecuteObject2(methodName, parameters, properties, configuration);
            break;
        default:
            throw new Error("The object " + objectName + " or method " + methodName + " is not supported.");
    }
}

//sample method that interrogates the method being called for object 1 and
//then calls the appropriate helper method
async function onexecuteObject1(methodName: string, parameters: SingleRecord, properties: SingleRecord, configuration: SingleRecord): Promise<void> {
    switch (methodName) {
        case "get":
            await onexecuteObject1Get(parameters, properties, configuration);
            break;
        case "create":
            await onexecuteObject1Create(parameters, properties, configuration);
            break;
        case "update":
            await onexecuteObject1Update(parameters, properties, configuration);
            break;
        case "delete":
            await onexecuteObject1Delete(parameters, properties, configuration);
            break;
        default:
            throw new Error("The method " + methodName + " is not supported.");
    }
}

//sample helper method. Note use of postResult to send the object back through the stack
function onexecuteObject1Get(parameters: SingleRecord, properties: SingleRecord, configuration: SingleRecord): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        //this is where you might do a var xhr = new XMLHttpRequest(); to send a request
        //for this example we are just creating the object. Note the object properties match an object definition
        //defined in ondescribe
        try {
            //postResult(obj) passes a single record back to K2
            //postResult(array) passes multiple records back to K2
            //The object or array must contain objects that have the same structure defined in the
            //object passed to postSchema() for the method being executed
            postResult({
                "id": 1,
                "title": "some text value",
                "completed": true,
            });
            resolve();
        } catch (e) {
            reject(e);
        }
    };
});

You are not required to use async functions as demonstrated in the template project, however this is the recommended approach. You can use async for ondescribe and onexecute to write unit tests, but keep in mind that there is no way to run your broker outside of the K2 environment, so you may also want to write tests using the SmartObject runtime API.

Package.JSON (and other supporting files)

You can see and can add other packages to your JSSP project by including them in the package.json file under the devDependencies node. For example, notice the k2-broker-core NPM package dependency in the sample projects - this file provides intellisense while you are writing code.

You do not need to use a package.json file (think of it like a .csproj project file), but it does make it easier to manage and maintain your broker and is the recommended approach. You can also use the K2 JS Broker Template which includes more extensive supporting files and packages, including the ability to run tests. It is based on the Todo object of the JSONPlaceholder service. Keep in mind that you can use multiple source files, and can split your ondescribe and onexecute methods into separate files for maintainability. But keep in mind that the build process must result in a single, bundled .js file.

About Programming with JavaScript

If you're not used to programming in JavaScript, you may need to familiarize yourself with a few concepts first before writing JavaScript-based service brokers. 

Keep in mind the following recommendations:

  • You can use awaits, async, and promises. 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
  • 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.

  • You should not use synchronous i/o on your XHR requests. JSSP does not support this
  • Dates that return Not A Number (NAN) in JavaScript are handled as NULL values in K2. Keep this in mind if you see null dates where you expect to see a date value.

  • Do not use namespaced values for properties and methods; this results in errors when associating generated SmartObjects. Example of flatting a Twitter response by using dots for the payload and concatenating likes:
    Copy

    Example of flatting a Twitter response

    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();
    }

Using a Postman Collection to generate code

Developing a JSSP-based broker can be challenging because you cannot easily build and execute code. You can write tests using the provided tests.ts file using the broker template sample project, but sometimes it is easier to start with the XHR code that Postman generates, because you know that it works and can troubleshoot the requests and inspect the return payloads.

For example, consider the following Postman collection that brings back a list of messages in an inbox using the Microsoft Graph API:

You’ve confirmed that this request is successful, and you want to do the same thing in your JSSP broker. Click the Code link below the Save button. This opens the Generate Code Snippets window that allows you to select what type of code you want to see. Click JavaScript – XHR and you’ll see the code. This allows you to get a jump start on the code that you’ll use in your OnExecute part of your JSSP broker.

When using this approach to generate code, bear in mind that you must still modify the code that you get out of Postman to use it in your JSSP broker. Also keep in mind that while you can embed a Bearer token in your JSSP broker, it’s not the best approach. Set up an OAuth resource to do the authentication flow, and then configure your service instance to use that resource. Hardcoding a bearer token directly in your JavaScript code should be used only during development and testing.