Working with Commerce Data: Elevated Permissions, Searches, SuiteScript 2.0 and Bootstrapping

This post was written for Kilimanjaro but applies equally to Vinson and Elbrus. It can apply to older versions too, although it mentions service controllers which replaced manually written service files from Vinson onwards.

A few weeks ago, we took a look at the basics of data in SuiteCommerce Advanced, including getting to know the nature of services, service controllers, and models. After going through that post you should have an excellent grounding in this stuff, allowing you to get stuck into them. However, there are still nuances and more complex ideas that can be investigated.

On November 1, 2017, we ran a webinar on working with data on your SuiteCommerce site where we talked about a number of things to do with data and SCA, as well as those aforementioned nuances and complex ideas. In this post, I want to reiterate those points to provide a companion piece focusing on:

  1. A quick look at the basics (services, service controllers and models) — more detail is available in the older post
  2. Why you might need to use elevated permissions
  3. Searching for data using saved searches and script-level searches
  4. Building a service that can call correlated items and create a merchandising zone for them
  5. Some simple differences in SuiteScript between versions 1.0 to 2.0
  6. How to call a (SuiteScript 2.0) Suitelet from a backend model
  7. How to bootstrap data and JavaScript so that it loads first
  8. A quick look at modifying the configuration record

Once you've gone through all of these points, you should have a far more advanced understanding of how to interact with data on your SCA site.

The Basics (Quickly)

I refer you to the earlier blog post if you want to go through this stuff in more detail, so let me quickly go through it.

When you have data stored in NetSuite, or you want to send data to it, there are a number of layers that you go through. For example, if a user submits form data then the request starts in the template, which then sends it to the view, to the frontend model, to the backend model and then to NetSuite via a service file (which has either been manually written or automatically generated). It is the models that handle data, and the view and template that present it.

Services are what make the connection between the NetSuite databases and your site. In these files you write SuiteScript, which is our data API, whose syntax resembles JavaScript. In modern (Vinson+) versions of SCA, we have service controllers to generate service files, which makes the process easier. As with the frontend views, models and routers, there are also 'base' models and service controllers that you extend to add your own methods to. Effectively, these are all standardized ways of writing services.

When you have the data, you need to configure your frontend to handle it. If the data is for a single record, then a simple model/view combination is fine; if there are multiple records (eg a list) then you'll need a collection, which is like a container and repeats the model and view generation process for each record. When creating or editing a record, we have a standard form view that streamlines the whole process by connecting the form and view together with minimal work.

In other words, there's a lot of stuff in the SCA package to make creating, reading, updating and deleting records on NetSuite as easy as possible. So once you've got that down, let's take a look at making it more interesting.

Elevating Permissions

This is something that has come up before in our discussions, for example when we looked at building a random prize generator and when we introduced the store locator. The gist is that when scripts run, they are run against a default user or role (usually that of the currently logged in user, such as a customer). This is fine for most cases because we have defined the schema so that they can only access the types of things that they should need to. However, there are cases when these permissions are insufficient because a specific bit of functionality needs access to unusual records. This is a problem.

In situations where a user needs to access those records, we can elevate their permissions; specifically, we can elevate the permissions of the file/script that needs to access those records.

This functionality was first used in SCA to alleviate issues in the store locator functionality, which relies on location records which customers do not ordinarily have access to. After elevating their permissions to give them access to location records, they can then run queries on them and find the stores near to where they are.

Technically speaking, it allows you to mark certain service (.ss) or SSP (.ssp) files to be run with the permissions of a specific role. It's not only roles that a script can be run with, but you could specify that a user need not be logged in to run it, or that they can run it as an employee or partner, or as the member of a particular department, subsidiary or group. The important thing is that the elevation is done only at specific times, with a specific use in mind; in other words, only when necessary.

Example: Simple Location Service

To illustrate this, let's take a look at an example. While we could use the store locator module, I'd rather we looked at a pure example. I have coded a module whose sole purpose is to grab some data from a specified location record (the name, longitude and latitude). If you want to play along, you can download the source here: ExampleLocation@1.0.0.zip; don't forget to update the distro.json so that the module is registered in the modules object, as a dependency to shopping.js and in ssp-libraries. If you don't already have some location records, you will also need to create them (Setup > Company > Locations).

What this module does is accept a request in the form of a URL ending in #examplelocation/[id], look up the location record with that ID, and then return its longitude and latitude.

You can see the router accepts this URL, and that the view and template are set up to display the data. So how does it get it? In the backend model, we can see that we're building up a search of filters (the criteria) and the columns (the data we want back). When we have that, we submit a location record search and return the results.

, get: function (id)
  {
    var filters = [
      new nlobjSearchFilter('internalid', null, 'is', id)
    ]

  , columns = [
      new nlobjSearchColumn('internalid')
    , new nlobjSearchColumn('name')
    , new nlobjSearchColumn('latitude')
    , new nlobjSearchColumn('longitude')
    ]

  , search = nlapiSearchRecord('location', null, filters, columns);

    if (search && search.length === 1)
    {
      return {
        internalid: search[0].getValue('internalid')
      , name: search[0].getValue('name')
      , latitude: search[0].getValue('latitude')
      , longitude: search[0].getValue('longitude')
      }
    }

    else
    {
      throw notFoundError
    }
  }

You can deploy the code if you want to test this bit out.

After deploying, go to [your base URL]/examplelocation/[id], where the id is the internal ID of one of your location records. When you do so, you should see an internal error. If you open your browser's dev tools and examine the XHR for ExampleLocation.Service.ss you should see the following error message:

{"errorStatusCode":"403","errorCode":"ERR_INSUFFICIENT_PERMISSIONS","errorMessage":"Insufficient permissions"}

This means that you've made the request, the server understood it, but you don't have the right permissions to do it. This is to be expected: after all, why would we allow shoppers to query data about your company's locations? Thus, we need to give them permission in this specific case:

  1. Look up the service file in the backend (eg search for ExampleLocation.Service.ss) and edit its record
  2. Go to the Permission tab
  3. Check Enabled
  4. Select Advanced Customer Center from the Execute as Role dropdown
  5. Check Run Script Without Login
  6. Click Save

Back on the frontend, refresh your example location service page. When it refreshes, you should see something like this:

Neat!

If it still doesn't work, and you now instead get a 404 error with an "Impossible to parse backend error" messaage, then go and check that you've enabled the Run Script Without Login functionality.

The Advanced Customer Center role was introduced with the elevated permissions update as our suggested role for accessing the location data. You can check it out by going to Setup > Users/Roles > Manage Roles. In Permissions > Lists you will see that the role has been given View permissions for Locations.

Going forward, should you wish to implement your own service with its own special permissions, you can use this concept as a model. You can start by customizing the Customer Center and then modifying the permissions. If that doesn't have enough permissions, you can create a brand new role and simply select the right permissions from that.

If you want to know what each of the permissions grant access to (and you should) then you read the comprehensive spreadsheet that we include in the permissions documentation.

Remember, it is important to keep the scope of the access as limited as possible. When you elevate permissions you're creating a file that does a specific thing and then saying that when it's called they have a specific kind of access to the specified records. It would be much simpler to grant all users of that kind unrestricted access to those records all the time, but that could be bad for security; by following our best practices, you can minimize the risks.

Saved Searches and Script-Level Searches

So there are two main ways to search for data for use within SuiteCommerce: saved searches and script-level searches. We already performed a code-level search when we did the location search in ExampleLocation.Model.js, above. Let's take a look at how we can use saved searches, for example to generate to correlated items.

What are Correlated Items?

There are two kinds of items that you can associate with an item: related and correlated. Related items are those that you manually associate with a product and can be useful for linking together complimentary items, such as accessories. Correlated items are system generated links that are created when shoppers buy items together. For the purposes of this tutorial, I'm going to use the latter as my site has some data on this.

An important thing to keep in mind is that correlated and related items cannot be generated with the IDs of child matrix items: they can only be linked to the parent item. We will need to code around this.

The rough idea is this:

  1. Create a saved customer search that returns all live shopping ('abandoned') carts on your site
  2. Create a module that fetches this saved search
  3. Extract the internal IDs of all the items the current user has in their cart
  4. Perform a code-level search on these IDs so that we can get the parent IDs of these items
  5. Pass the parent IDs to the view, which then plugs them into a merchandising zone child view to return a slider of correlated items

To speed things up, I'll give you the code to the module up front: ExampleCorrelated@1.0.0.zip. You can unzip it into your customizations directory, update your distro.json file to add the module to the various dependencies objects. After that you can deploy it, but it won't work yet.

Create the Saved Search

Go to Lists > Search > Saved Searches > New. Create it as follows:

  • Title — Abandoned Carts
  • ID — _abandoned_carts
  • Public — (checked)
  • Criteria > Standard — Shopping Cart : SubTotal is greater than 0.00
  • Results > Columns — Shopping Cart : Item ID

Elevate Permissions

As we discussed, we need to elevate permissions on this file. However, while the Advanced Customer Center role covers one part of what we need to do, we also need to give the script the ability to look up private item data too.

Go to Setup > Users/Roles > Roles and then customize the Advanced Customer Center role. This will create a new role, using the Advanced Customer Center role as a template.

The name and ID of this aren't important — you can set them if you wish — but what is important is this setting:

  • Permissions > Lists — Items : View

This will give us the additional permission that we need.

Code Analysis

OK, now you can test it out. Deploy, if you haven't already, and then login an account that has items in its cart and where those items have correlated items. If, like me, you always check out with the same items, I'd suggest you add those to your cart to test.

When you're in your account, change the URL hash to #examplecorrelated and you should see something like this:

The IDs and products will be different, but you should see something. Now, head over to SuiteScript > ExampleCorrelated.Model.js in the module code.

Here, there are two main functions: one that calls the saved search to get the IDs of the items in the user's cart, and another to take those IDs and do a search for parent items that have those items as children.

To call a saved search, this code uses a helper function we've built into the SCA code, Utils.loadAndMapSearch. You can look this up in the backend utility file if you wish, but it's just an easy way of making the call once you're ready to. Once we have those results we process them to make a neat array out of them. Note that I'm using _.map() here, but as you'll see later on you could just use a simple for loop. The _.map() method is handy when you need to construct an array of object and pops up frequently in backend model files.

Once we have those IDs, we send them to our second function. As a demonstration, rather than generate another saved search to get the IDs, this time I'm showing you to perform an ad hoc records search. Quite simply, the filter is going to search all of our items to see if their internal IDs match any of the ones we just got from our saved search. If it finds some then it'll return the internal ID of the parent item (the 'column' value).

Note that when we perform the search, it is here that we need to the Item > View role permission. Without it, we'd get an error saying that we don't have the right permissions. Once we have the search results, we think do a for loop to build up a simple array of IDs of the parent items.

And that's kinda it for the data side of things. From here we follow the normal procedure of just passing the array of values back to the frontend aspect of the site, which is where we plug it into the merchandising functionality.

Merchandising Zones

We include merchandising functionality in the SC source code. Of interest to us is ItemRelations.Correlated.View. You can look at the source code for it if you wish, but the crucial parts that we need are as follows:

  1. In our view, we can add it as a dependency
  2. Then we can add it as a child view
  3. The child view accepts two parameters: one for the application (which is standard) and the other itemsIds, which is either a single ID or an array of IDs that we want to generate correlated items for

By plugging in the array of IDs, an item search is being performed. For this search, it specifies a field set (correlateditems_details) which is specifically set to return the correlated items. To get a sense of what data is being returned, you can actually perform the API call in your browser. Substitute in parameter values for your own site but it should look something like this:

[domain]/api/items?fieldset=correlateditems_details&include=&language=en&country=US¤cy=USD&pricelevel=5&c=[company id]&n=3&id=6594%2C8033%2C8044%2C8050

The code in the merchandising functionality will take each of those items and render it in a slider.

SuiteScript 1.0, SuiteScript 2.0 and Suitelets

SuiteScript has been around for a while now and since its inception we've added new objects and methods to it. However, you may be aware that we've also been developing its replacement.

If you've not taken a look at it yet, then you should know that its style is similar to what we've done with the JavaScript in SCA: you create a module, create an entry point file, start with a define statement, etc. Additionally, all methods and inputs are JS objects, and we no longer prefix them with nlapi and nlobj. Instead, you get something like record.Type.SALES_ORDER.

I bring this up because it's a good thing to keep in mind: eventually, SCA will be migrated to 2.0 and you'll need to learn how to use it. None of our SCA modules are currently SuiteScript 2.0 and, at the time of writing, it is not possible to use 2.0 code in an SCA module. You can, however, write an ad hoc 2.0 client script in a similar vein to the ones we did for our scriptable cart demo. There is a hello world tutorial in the help center.

We can, however, have a go at it ourselves by setting up a simple service in a module that then calls a SuiteScript 2.0 Suitelet, which does some work. In this scenario, we're going to ask the Suitelet to fetch us some data about the current user, such as their subsidiary, country, city, etc.

I should add that it is not typical to call Suitelets within service files on an SCA site. Generally, we advise against it as it is more efficient to do the normal thing of calling whatever data you require through service controllers and the commerce API. Furthermore, Suitelets can't access any of the goodies associated with the SCA code, such as Utils functions.

I have prepared an example module for you: ExampleSuitelet@1.0.0.zip.

In a folder I helpfully named ADD ME TO THE BACKEND is a Suitelet file written in Suitescript 2.0. If you take a look you'll see that its structure is eerily reminscient — that's because it follows AMD specification.

/**
 * @NApiVersion 2.x
 * @NScriptType Suitelet
 */

define(
  [
    'N/search'
  ]
, function ExampleSuitelet
  (
    search
  )
{
  function loadSomething(context)
  {
    var customerId = context.request.parameters.customer;

    var data = search.lookupFields({
      type: search.Type.CUSTOMER
    , id: customerId
    , columns: ['subsidiary', 'country', 'city', 'phone', 'mobilephone']
    });

    context.response.setHeader({
      name: 'Content-Type'
    , value: 'application/json'
    });

    context.response.write({
      output: JSON.stringify(data) || {}
    });
  }

  return {
    onRequest: loadSomething
  };
});

We start by adding the search API as a dependency before creating a function called loadSomething. Note that it can be named virtually whatever you want, but it must be named. At the end of the file, we're going to return a call to this function.

What this function does is specify the type of search we want to do, using the customer ID that's been passed to it via the request URL (we'll look at this in a moment), and then what columns (fields) we want back.

That's the 'work' part of the script, the next parts: setting the headers (note we're specifying JSON content type) and the response, which is just a stringified version of the returned data.

In order to use this script file, you will need to create a script record for it (Customization > Scripting > Scripts > New, which for the purposes of this tutorial I'm recommending you call _example_suitelet. You then need to create a deployment record for it, which I'm going to suggest we call (again) _example_suitelet.

With that uploaded and ready, you can now take a look at the module code in the zip. We again have a simple module set up to handle URL requests to #examplesuitelet. And, again, the meat is in the backend model file.

, get: function () {
    var stSuiteletUrl = nlapiResolveURL('SUITELET', 'customscript_example_suitelet','customdeploy_example_suitelet', true);
    stSuiteletUrl = stSuiteletUrl + '&customer=' + nlapiGetUser();

    var headers = new Array();
    headers['Content-Type'] = 'application/json';
    headers['User-Agent-x'] = 'SuiteScript-Call';

    var response = nlapiRequestURL(stSuiteletUrl, null, headers, 'GET');

    return response.getBody();
  }

We start by building up a request URL for the Suitelet, which means using nlapiResolveURL and specifying that we're calling a Suitelet, named customscript_example_suitelet, and deployed using the customdeploy_example_suitelet record. You'll note that we then attach the user ID to the end, which we needed in our Suitelet for the lookup.

Then we set the headers (important) and then submit the URL; the response's body is then returned as the result of this function. In total, it returns this:

Calling a Suitelet within an SCA module is pretty unconventional, so we wouldn't really say it was best practice from a purely SuiteCommerce point of view. However, if you're building an integration and you can re-use the Suitelet for something else, then this could be a good idea as it saves you from having to maintain two seperate files that do the same job.

Bootstrapping Data

Getting your site as fast as possible is a noble goal for any web developer. While you can optimize your code to make things better that way, there's one thing that's seemingly out of your grasp: data. After all, once your code loads you still have to wait for the server to send you the data records that you've requested.

One common issue that we receive feedback on comes typically from partners and system integrators. They have this thing that wants to retrieve a lot of data — whether from NetSuite or an external service — are frustrated by how long it takes to load, especially as this data is so important that it needs to be loaded early.

Requesting data from NetSuite via a service in SCA can be time consuming. First the main application must load, and then the services are loaded after; further more, running a record search or lookup can be costly as you're querying the databases.

The solution to this is to do bootstrapping.

Bootstrapping is the idea that you provide a system will all the things it needs to get started right away. An example of this is with your computer: a small, basic operating system is pre-installed on your motherboard that readies all the components when you power it on.

What this means for SCA is one of two things:

  1. Preparing your data beforehand (as JSON) and then sending it to your application as soon as possible — this will eliminate a service call, and make sure the data is ready immediately
  2. Injecting a call to a service right at the start of when the application is loading (ie ignoring our pre-defined order)

Typically, we don't recommend the second method as a way of speeding up your site, it's more for when you want a service called straight away (and on every page). To test these two approaches, we're going to make use of the shopping.environment.ssp file.

Library Files

If you look up an SSP application in the backend, you'll see that you can assign library files to it. Head over to the SSP application that runs your website (eg SuiteCommerce Advanced - Dev Kilimanjaro). You'll see that we associate some files with it.

These are 'bootstrapped' to the SSP and are loaded when the application file is.

We can add files to this too!

  1. Create a new file (anywhere really)
  2. Set a variable, eg var BackendData =
  3. Copy and paste in a large blob of JSON data (eg stuff you've gotten from the items API)
  4. Wrap the JSON data in JSON.stringify('')
  5. Save the file; it can be called whatever you want as long as it has the .js extension
  6. Edit the SSP application and in the library files section, upload your saved file

Once that file is up and ready, we need edit shopping.environment.ssp. Yes, while we don't normally recommend editing files in your source code, this is one situation where it's not possible to do this change without it.

So, open up the file and then put at the bottom:

SC.ENVIRONMENT.bootstrappedData = <%= BackendData %>

This adds a value to the SC.ENVRIONMENT global variable key of bootstrappedData. The value of this key will be the variable BackendData, which you'll remember is what we set in the library file.

With that data, it's just a case of doing JSON.parse(SC.ENVIRONMENT.bootstrappedData) and using the result, eg:

To reiterate, this method is particularly useful if you can pre-generate a large object of JSON data that would otherwise take a while to load via a service. Alternatively, it can be used kinda like an inline script tag, where you just load some arbitrary JavaScript that you might need for an integration. But, like I said, it's a bit hacky editing the environment file directly so only use it if it's the best way to do it.

SC.ENVIRONMENT.published

In the same file, there's actually another bootstrapping method built in via a function that sets values in SC.ENVIRONMENT.published. The way it works is to find values that have been attached to SC.Configuration.publish and then execute service method calls based on them. What this means is that when this file loads, you can make a service call on some arbtirary service file that you specify. Again, this is made (and then cached) whenever shopping.environment.ssp is loaded.

For example, see ExamplePublishPush@1.0.0.zip. There are two files: the model, which does nothing in particular other than return a value (which simulates the data you might want to set as a value) and another file called ExamplePublishPushThing.js, which we should look at it.

It's quite simple but after adding the configuration as a dependency, you then push an object into the array with a key, and then the model and method you want to call. If you include this file in your SSP distribution then it will be processed and that object will be pushed. You can see what the object is doing and how it relates to the arbitrary model included with the module.

When it's deployed and the environment file is called then we can access the data that the service file has returned.

Incomplete Data and Silent Failing

We are aware that there are some nuances to requesting data in NetSuite that can sometimes cause incomplete data to be returned and for this process to fail silently. Let me summarize some of those situations before going into detail:

  • The user has permission to access the record but does not have access to a record which is the source of one of those fields
  • The wrong subsidiary is being used (usually because you haven't specified one)
  • You have incorrectly assumed that elevating permissions applied to the user rather than a specific file
  • You haven't checked the Run Script Without Login box when you needed to
  • You've attempted to include elevated permissions in a bundle, but they can't be

Let's now take a look at those situations.

You don't need special permissions to access a custom record, but you will run into problems if one of those fields is sourced from a record to which you don't have access to (eg an order). The problem is that you will get all of the rest of data except for the data in that field (and you won't get warned that it's missing).

You can run into similar problems with subsidiaries: If you don't specify a subsidiary then the customer's subsidiary is used; if they're not logged in then the default one for the site is used. But you shouldn't rely on this, you should specify what subsidiary you want because different users may have different subsidiaries and you may end up with unexpected results.

Elevated permissions only apply to the file, not the user - you may assume that it is the customer's or that when you log into NetSuite it will use yours but it's not going to work that way.

If you don't check the Run Script Without Login box then the service will return a 404 error.

At the moment, we don't let you bundle in elevated permissions — yes, we do use this in the store locator functionality but it's not currently available for partners and third-parties. It's on our roadmap, but I can't say when it'll be released.

Configuration

Perhaps the final thing to consider is the configuration record. When creating a brand new piece of functionality, you can create new entries for an administrator to configure by providing JSON data. This is something that has come up numerous times throughout the tutorials on this site, and is apparent in our source code by taking a look at the contents of Configuration folders. After seeing the names of options, you can then search through the module's JavaScript files to see how that information is plugged into the code.

If you're unfamiliar with this stuff, a common way to get started is to add a switch that can enable or disable functionality, which you can find in the introductory post on the configuration tool. However, a more advanced feature is to modify existing configuration JSON... without modifying the files themselves.

The process for doing this is something we've talked about before and it relies on a technology called JSONPath. It lets you add, modify and remove entries from the final generated configuration manifest. In that post and in our use case examples documentation there are ideas about the sorts of things that you can do:

  • Add a new default property to an array
  • Add text to an existing string
  • Add a new default option to an array
  • Change the default value of a property
  • Change the label of a subtab
  • Remove a configuration option
  • Hide a property from the user interface

The instructions in the documentation on how to accomplish these samples is detailed and it's not worth rehashing here.

Accessing the Configuration Object

I just want to add a quick note: the final configuration object, with all the values that has been set in the backend, is available as a global variable. If you open the developer console on your site and type in SC.CONFIGURATION, you will get the object returned to you. In your code, of course, you should instead add it as a dependency and call its values that way.

Where the Manifest and Record Live

I am going to refer you to the previously mentioned introductory post on the configuration tool to highlight where the manifest (ie, the raw options without values) and the record (the manifest populated with the site's values) live. There's good advice in there.

Final Thoughts

This article is a follow-up not only to the webinar that we did but also to my earlier article that I wrote about data. I hope that it has elucidated some technical part of the platform; in particular the stuff around SuiteScript 2.0 and bootstrapping. While neither of them are things that you should jump on straight away, I hope I've communicated clearly when they might be appropriate for you. For example, in most cases you don't need to bootstrap your data — it's really only applicable when you've identified a particular problem with slow data and you know that you can generate a JSON blob and use that as a placeholder.

This has been a look at more advanced aspects of SCA data; if you need more basic explanations, take a look at an introductory post I wrote and the aforementioned getting started guide.

Further Reading

In addition to the links inline above, the following pages might be useful to you: