Develop Your First SuiteCommerce Advanced Module (Kilimanjaro) - Part 3: Create, Read, Update and Delete Records

This tutorial is the third in a four-part tutorial on the basics of developing a SuiteCommerce Advanced site. It is intended for the Kilimanjaro version, but can be adapted to work with Vinson and Elbrus. If you're using Denali or Mont Blanc, refer to our older tutorial.

The second part of the tutorial focused on the basics of data: services (and service controllers), models (backend and frontend) and collections. We also introduced new non-data concepts, such as child views and Underscore functions.

In this part, we're going to use expand to use real data, as well as implement the full complement of HTTP/CRUD functions so that the user can create, read, update and delete their personal preferences.

Custom Record Type

Within NetSuite, we have a lot of records — they're the things the hold data. As part of our offering we have a lot of built-in 'standard' record types, things like sales orders and inventory items. However, the platform also allows you to create your own types — and that's what we're going to do with our functionality. We want to record user preferences, so we're going to need to create a custom record type with custom fields to capture them.

In NetSuite, go to Customization > Lists, Records, & Fields > Record Types > New and then set it up as follows:

  • Label: User Preferences
  • ID: _user_preferences
  • Include Name Field: (unchecked)
  • Access Type: Use Permission List
  • Show ID: (checked)
  • Permissions Tab:
    • Role: Customer Center
    • Level: Full
    • Restrict: Viewing and Editing

Make sure you click Add in the Permissions tab to add that permission to the table, and then click Save to add the record type. Leave all other settings on their default values.

So we've just created a new record type. Some important things to note before we move on: first, the ID starts with an underscore. In NetSuite, all custom things you create will be automatically prefixed with a standard name depending on what type it is. In our example, customrecord will be prefixed, resulting in our record having the reference of customrecord_user_preferences — hence the underscore before the name.

Next, the permissions are set up so that customers will be able to read, edit and delete only the records they own.

After saving, the page reloads and the Fields tab should now be visible. We're now going to create the fields we need to store data in.

For each of these, click New Field to start:

  • Label: Owner
  • ID: _user_preferences_owner
  • Type: List/Record
  • List/Record: Customer
  • Validation & Defaulting Tab:
    • Mandatory: (checked)

This one will be used to store the ID of the user who created it. This is how we'll know what records the shopper can access.

  • Label: Type
  • ID: _user_preferences_type
  • Type: List/Record
  • List/Record: New
    • Name: Type
    • ID: _user_preferences_type
    • Values: Color, Size
  • Validation & Defaulting Tab:
    • Mandatory: (checked)

This'll be used to store the type of preference. We're creating a custom list for this because we want to limit the values that the user enters. In other words, we're going to create a dropdown from which users can select the value they want.

  • Label: Value
  • ID: _user_preferences_value
  • Type: Free-Form Text
  • Validation & Defaulting Tab:
    • Mandatory: (checked)

Allowing users to their size or color as a string rather than from pre-determined list is a little sub-optimal. After all, we just said we want to limit the user's inputs, but to be perfectly honest it would complicate things quite significantly, detouring us from the purpose of this tutorial. So, yes, therefore, we are going to stick with free-form text.

Create Records

So the first thing we can learn is the process for adding new records. The code we're going to write is SuiteScript: this is the proprietary scripting language that only runs on NetSuite servers. There are multiple facets to SuiteScript, and a lot of nuances to learn, so I'm going to ignore all of them to just focus on the stuff we need for this module.

In SuiteScript, there are three parts to creating a new record:

  1. Specifying what type of record you want to create
  2. Setting the field values
  3. Submitting the record

If you think about it, this process is mirrored in the backend UI too.

Add a New Method to the Backend Model

In SuiteScript > UserPreferences.Model.js, add the following method:

, create: function (data)
  {
    var newRecord = nlapiCreateRecord('customrecord_user_preferences');

    newRecord.setFieldValue('custrecord_user_preferences_owner', nlapiGetUser());
    newRecord.setFieldValue('custrecord_user_preferences_type', data.type);
    newRecord.setFieldValue('custrecord_user_preferences_value', data.value);

    return nlapiSubmitRecord(newRecord);
  }

As we've alluded already, talking to NetSuite via our APIs involves sending and receiving data in JSON; creating records via SuiteScript is no different.

We start with nlapiCreateRecord, which doesn't actually 'create' the record per se; rather it readies the creation of a new record, kinda like asking for a form so that you can fill it in.

The next part is filling it in, ie, setting the field values. The first — the owner — can be determined automatically by using nlapiGetUser(); this is a method that returns the ID for the current user, and is vital for accurately ascertaining ownership throughout the system.

The rest of the fields will be determined by what's passed to the create method via the data argument. You'll note that we use the method setFieldValue — this is standard and must be used when creating new records.

The final part is the nlapiSubmitRecord call, which has the completed record object passed to it. This'll create a new user preferences record with the owner and user's values set.

But, of course, this won't do anything just yet because it's not connected to the frontend. So now, we need to connect it to the service controller, so it knows to call it when a POST HTTP call is made.

Add a New Method to the Service Controller

We need to call the create method in the model when asked to; but how does the system get asked in the first place? By adding a post method to the service controller.

In UserPreferences.ServiceController.js, add the following method:

, post: function()
  {
    this.sendContent(UserPreferencesModel.create(this.data), {'status': 201});
  }

The sendContent method is a part of the application which sends data to the servers; so what we're saying is, "Send content to the server using the create method of the UserPreferences model, with this data. Afterwards, return the 'created' HTTP code." This is all done with the right headers and content type, so you don't have to worry about that.

Next we need to call this method from the frontend.

Update the Router

Remember, we called the GET method by hitting a route on the frontend. For submitting (and updating and deleting) record data, the story is the same: we just need new routes and functions in our router.

For now, we're going to test this works by submitting dummy data — we'll build an actual form to submit user data later.

Open up UserPreferences.Router.js and add a new dependency: UserPreferences.Model as UserPreferencesModel.

Then, add a new method:

, preferencesAdd: function ()
  {
    var model = new UserPreferencesModel();

    model.set('type', 1);
    model.set('value', 'Orange');

    model.save();
  }

We already added a route for this function. So, when a user visits preferences/add, it'll be called. It'll create a new model and add some dummy data to it, and then it'll run its save method. This method is built into Backbone and our application to trigger the service specified in the model we specified.

We can now do some testing. Do a deploy of the code up to your site. When it's done, log into the dummy account on the frontend and navigate to #preferences/add. You should see a completely white page but — and here's the important thing — if you check the UserPreferences.Services.ss call in the Network tab, you should see the call being made. The Request Payload in the Headers tab should show you the data being sent, and, in the Response section, an integer — this is the server returning the internal ID for the newly created record! You can hit refresh a few times to create new a record each time.

To check it, log into the backend of your site and go to Customization > Lists, Records, & Fields > Records. Click List in the User Preferences, row click List see your freshly created records!

List Records

Now that we have the records in the system, we can look at pulling them out and displaying; ie, perform a proper GET request.

To get the data we want from NetSuite, we need to perform a record search, which is performed by running nlapiSearchRecord in a backend file. It is a method that accepts four arguments:

  1. type: the internal ID of the record type we want to perform a search on
  2. id: the internal ID of the saved search (we're not using a saved search, so we can ignore this)
  3. filters: an object or array of objects that reduce down the number of results returned (eg, we're going to specify an owner so we only get the records the current user owns)
  4. columns: an object or array of objects specifying which parts (ie fields) of the returned records we want

As we're dealing with arrays of objects, it's generally a good idea to build these up as variables before performing the search. Thus, in SuiteScript > UserPreferences.Model.js, replace the list method with this:

, list: function ()
  {
    var type = 'customrecord_user_preferences';

    var filters = [
      new nlobjSearchFilter('custrecord_user_preferences_owner', null, 'anyof', nlapiGetUser())
    ];

    var columns = [
      new nlobjSearchColumn('internalid')
    , new nlobjSearchColumn('custrecord_user_preferences_type')
    , new nlobjSearchColumn('custrecord_user_preferences_value')
    ];

    var search = nlapiSearchRecord(type, null, filters, columns);

    return _.map(search, function (result) {
      return {
        internalid: result.getValue('internalid')
      , type: result.getText('custrecord_user_preferences_type')
      , value: result.getValue('custrecord_user_preferences_value')
      };
    });
  }

The id is pretty self-explanatory: we specify the types of records we want.

With the filters, we create a NetSuite object using nlobjSearchFilter, which has five parameters:

  1. name: the internal ID of the filter (eg the field name)
  2. join: the internal ID of the joined search where this filter is defined (we're not using it, so it's null)
  3. operator: the operator name we're using to do with the filtering (eg anyOf, contains, lessThan) — this is the type of matching we want with the values provided for filtering (for lists and records, we have to use anyof or noneof)
  4. value: the value you're filtering by
  5. value2: an optional second value that we're not using

After that, we have the columns. These represent the field data we want returned our search. For these we construct a different NetSuite object called nlobjSearchColumn, supplying the IDs of the fields we want. (Fun fact: nlobjSearchFilter and nlobjSearchColumn are the only two objects that can be constructed — all other objects can only be created via top-level function or method calls.)

The final thing to do is perform the search, with nlapiSearchRecord and newly created variables. A key thing to note here is that we assign it a variable too: this makes it much easier to manipulate once the data comes back — if we didn't do this, every time we did something we'd end up making another call to the API, which is pointless, time-consuming and resource-consuming.

So what it's saying is, "Perform a search on user preferences records, filtering for ones that the current user owns, returning to me only the internal ID, type (ie color or size) and value of that type (eg orange)".

Once we have those records, we create an array out of the results using _.map. It is one of my five crucial Underscore functions and is useful because it takes the array results and then creates a new, more usable one, based on the transformations we provide it. In our case, we're creating a simple array of objects where each one contains the internal ID, type and value.

One important thing to note here is that for internalid and value, we're using the getValue method but when it comes to type, we're using getText. If we used getValue instead, we'd get the internal ID of the value in the select (dropdown) field instead, which isn't user friendly. In other words, this way we get back the string Color rather than the integer 1 (which is its internal ID).

Great! Now if you deploy up to NetSuite and then refresh the #preferences page, you should see your records returned!

A Form for Creating New Records

At the moment, new records are created using dummy data every time a particular page is hit. This, obviously, isn't how we want our functionality to work. Instead, we want to have a form that the user can fill out and then submit.

Add an Edit View

In JavaScript, create UserPreferences.Edit.View.js and in it put:

define('UserPreferences.Edit.View'
, [
    'Backbone'
  , 'Backbone.FormView'
  , 'underscore'
  , 'user_preferences_edit.tpl'
  ]
, function
  (
    Backbone
  , FormView
  , _
  , user_preferences_edit_tpl
  )
{
  'use strict';

  return Backbone.View.extend({
    template: user_preferences_edit_tpl

  , events:
    {
      'submit form': 'saveForm'
    }

  , initialize: function (options)
    {
      this.model = options.model;
      this.application = options.application;
      FormView.add(this);
    }

  , typeOptions: function ()
    {
      var options = [
        {internalid: 1, name: _.translate('Color'), isSelected: (this.model.get('type') == 'Color')}
      , {internalid: 2, name: _.translate('Size'), isSelected: (this.model.get('type') == 'Size')}
      ]

      return options
    }

  , getContext: function ()
    {
      return {
        typeOptions: this.typeOptions()
      }
    }
  })
});

This looks like a regular view except we're using two new things: a form view and events.

The form view is a view we created to standardize the forms we use across SCA. Essentially, it does all the heavy lifting in regards to connecting the form to the application: all you have to do is plug it in and switch it on. After adding it as a dependency, all we need to do is add a call in the initialize method.

Next, is the events object. This is available in views and allow you to create listeners following the "event selector": "callback" pattern provided by the jQuery.on() method.

In our view, we're adding a listener for when a submit button is pressed within in a form element. When it's triggered, it will run the saveForm function, which is attached to the form view. Essentially, this is what will transfer the data along the chain up to NetSuite.

For more information on events, take a look at how to get to grip with Backbone events.

The rest of the file is dedicated to the type options for the select/dropdown element. We need to send these to the template so that we can generate the dropdown's options. Note that I've used the same internal ID and names as the backend values.

Also note that I've done an awful thing of comparing the value of text strings to see which one should be marked as 'selected' in our template. Obviously don't do that in your code, but like I said earlier, this bit would normally be done a different way. So let's gloss over it.

Add an Edit Template

We mentioned a new template in our view, so let's create it now.

In Templates, create user_preferences_edit.tpl with:

<h1>{{translate 'Add/Update User Preference'}}</h1>
<form>
    <fieldset>
        <label>{{translate 'Type'}}</label>
        <select name="type" id="type">
            {{#each typeOptions}}
                <option value="{{internalid}}">{{name}}</option>
            {{/each}}
        </select>

        <label>{{translate 'Value'}}</label>
        <input type="text" name="value" id="value" value="{{value}}">
    </fieldset>
    <button type="submit">{{translate 'Add/Update'}}</button>
</form>

A standard, simple form with an interesting Handlebars helper: {{#each}}. This is a simple block that iterates over every item in the list and does something. In our case, we want to produce an option for the dropdown where the internal ID is the value and the name is the text that's displayed.

Update the Router

Finally, we need to trigger the view to load when the route is called.

In UserPreferences.Router.js, add UserPreferences.Edit.View as a dependency (as UserPreferencesEditView), and then replace the preferencesAdd method with:

, preferencesAdd: function ()
  {
    var model = new UserPreferencesModel();
    var view = new UserPreferencesEditView
    ({
      model: model
    , application: this.application
    })

    view.showContent();
  }

This looks similar to the list route below it, so you should be familiar with the pattern: we ready the model (not a collection, because we're only dealing with one record, remember), ready the view, and then call showContent(), which will render it (and thus the template!).

Stop and start your local server and visit #preferences/add. When it loads, you should see a form like this:

Complete it with some dummy data, eg with the color red, and hit the Add/Update button. You won't get a confirmation message (yet) that it's gone through but you can check the request payload of the service in the Network tab to see that it's sending data; and you can check the response from the server, which will be the internal ID of your newly created record.

You can also return to the list page and see your newly created records in the list!

Get Individual Records

So, we can create records and read them as part of a list, but what about viewing them individually? Well, we're on the road to updating record data and we can actually use a lot of the similar code.

Add a Get Method to the Backend Model

Before we can update records, we first need to be able to view them directly.

Open SuiteScript > UserPreferences.Model.js and add:

, get: function (id)
  {
    var type = 'customrecord_user_preferences';

    var filters = [
      new nlobjSearchFilter('custrecord_user_preferences_owner', null, 'anyof', nlapiGetUser())
    , new nlobjSearchFilter('internalid', null, 'is', id)
    ];

    var columns = [
      new nlobjSearchColumn('internalid')
    , new nlobjSearchColumn('custrecord_user_preferences_type')
    , new nlobjSearchColumn('custrecord_user_preferences_value')
    ];

    var search = nlapiSearchRecord(type, null, filters, columns);

    if (search && search.length === 1)
    {
      return {
        internalid: search[0].getValue('internalid')
      , type: search[0].getText('custrecord_user_preferences_type')
      , value: search[0].getValue('custrecord_user_preferences_value')
      }
    }
  }

The first thing you'll note is that it is very similar to the list method — and why wouldn't it? They're effectively doing the same thing. However, the crucial differences are:

  1. Getting a single record rather than a list, requires/allows us to look up a record using its internal ID, which we specify as a filter
  2. Rather than map the results (ie produce an array of objects), we return a simple object of the fields we want

Now, we already have the code in our service controller to handle this, so we can move on.

Update the Router with an Edit Route

Back in UserPreferences.Router.js, you'll see that we already added a route for when a user visits #preferences/:id, so now we need to add the function to support this.

Add a new method:

, preferencesEdit: function (id)
  {
    var model = new UserPreferencesModel();
    var promise = jQuery.Deferred();
    var application = this.application;

    if (!id) {promise.resolve()}
    else
    {
      model.fetch({data: {internalid: id}})
      .done(function () {promise.resolve();});
    }

    promise.done(function ()
    {
      var view = new UserPreferencesEditView
      ({
        application: application
      , model: model
      });

      view.showContent();
      view.model.on('sync', function (model)
      {
        Backbone.history.navigate('preferences', {trigger: true});
      });
    });
  }

This is similar to the preferencesAdd method; in fact it's so similar, I want you to delete the preferencesAdd method, and update the routes object so that the preferences/add route points to preferencesEdit.

Now you've done that, we can look at the new code.

We're using promises, or, more specifically, jQuery's deferred objects. Now, there's a lot to learn about these, if you're interested, but let me tell you the crucial stuff. When you do something like make an API call, you want code to run depending on the result of that call. Now, you could write a series of complicated callbacks that checks the status (while you wait) and then does stuff, or you can use a deferred object.

Essentially you create a placeholder, and then attach handlers to it. In our example, we want to see if an internal ID has been provided: if it has then we want to fetch the data of that ID; if it hasn't, then we can skip that step. In either case, we then render the view and then set up a listener so that whenever a model is synced with the server, we trigger a redirect to the list page.

Got it?

OK, so the deferred objects come in to play with the promise.resolve and promise.done bits. Running .resolve() executes the code in the .done() function — and in our conditional statement, both cases will run it. However, in the case of an ID being provided, we run a fetch on the data based on the internal ID first.

The code works because, just like before, if there's no data, then all we need is to ready an empty model. However, if there is data, then we can just send it to the view so it can be used in our template.

Add the Data to the Edit View

We've called the data, now we need to send it to the view (and then on to the template).

As the data will already be passed to the view, now we just need to add it to the context object so the template can get at it.

Update the getContext method:

, getContext: function ()
  {
    return {
      typeOptions: this.typeOptions
    , type: this.model.get('type')
    , value: this.model.get('value')
    }
  }

Now, we already coded the template to handle this data. If you remember, our inputs are already marked up with code like value="{{type}}", which means that when we view the form and it's associated with an existing record, then the fields will be pre-populated when they load.

Update Individual Records

OK, nearly there.

Add an Update Method to the Backend Model

Head back to the SuiteScript > UserPreferences.Model.js and add a new method:

, update: function (id, data)
  {
    var record = nlapiLoadRecord('customrecord_user_preferences', id);

    record.setFieldValue('custrecord_user_preferences_type', data.type);
    record.setFieldValue('custrecord_user_preferences_value', data.value);

    return nlapiSubmitRecord(record);
  }
});

nlapiLoadRecord takes the type and ID of a record and gets its data. Then all we all need to do is set the field values and then submit it back to the system. Easy.

Add a Put Method to the Service Controller

So now we connect the backend model to the service controller. For this, open up UserPreferences.ServiceController.js and in it put:

, put: function ()
  {
    var id = this.request.getParameter('internalid');
    UserPreferencesModel.update(id, this.data);
    return UserPreferencesModel.get(id);
  }
})

This is a familiar scene: take the internalid from the request, take the data, and then pass it to the model and run the update method.

That's it! Save everything and then deploy it up to the servers. When that's done, you can head over to your site and test out the different routes and the different methods available to you.

But there's one method left: deleting a record.

Delete a Record with a Modal

As part of the SuiteCommerce Advanced offering, we have a number of built-in modules that standardize the type of functionality you'd expect to see on a website. One of those is modal windows: modern pop-ups that show information in a window, usually for a 'quick view' of something (eg products) or for confirmations. We're going to use this functionality to ask the user if they're sure they want to delete a record.

Add a Delete Method to the Backend Model

To start, let's focus on the backend. There isn't a lot of complicated code to add; it's quite simple actually.

Open up SuiteScript > UserPreferences.Model.js and add:

, delete: function (id)
  {
    nlapiDeleteRecord('customrecord_user_preferences', id);
  }

Add a Delete Method to the Service Controller

Next, to connect it to the service controller, open up UserPreferences.ServiceController.js and add:

, delete: function ()
  {
    var id = this.request.getParameter('internalid');
    UserPreferencesModel.delete(id);
    return {'status': 'ok'}
  }

Add a Button to the Details Template to Trigger Record Deletion

We haven't added any buttons for our actions yet (we'll do that in the next part), but we need one for the delete action as we don't have a route or any other way to trigger it.

Open user_preferences_details.tpl and add a new cell to the table row:

<td><button data-action="delete" data-id="{{internalid}}">{{translate 'Delete'}}</button></td>

Add a New Column to the List Template

We added a new button but nowhere for it to live.

Open up user_preferences_list.tpl and add the following the table head:

<th>{{translate 'Actions'}}</th>

Update the List View

This is the meat of the new functionality: this is where we connect the event of clicking the button to trigger the modal confirmation window and then the delete method in the service.

Open UserPreferences.List.View.js, and, in the initialize method, add:

var self = this;
this.collection.on('reset sync add remove change destroy', function() {
  self.render();
});

What this does is listen for any changes to the collection; if something is added, deleted, changed, etc, then it'll re-render the collection.

In the same file, we need to add some new dependencies: GlobalViews.Confirmation.View as ConfirmationView, and jQuery as jQuery.

Then add:

, events:
  {
    'click button[data-action="delete"]': 'removeUserPreference'
  }

, removeModel: function (options)
  {
    var model = options.context.collection.get(options.id);
    model.destroy();
  }

, removeUserPreference: function (e)
  {
    e.preventDefault();

    var view = new ConfirmationView
    ({
      title: 'Remove Preference'
    , body: 'Are you sure you want to remove this preference?'
    , callBack: this.removeModel
    , callBackParameters:
      {
        context: this
      , id: jQuery(e.target).data('id')
      }
    , autohide: true
    });

    this.application.getLayout().showInModal(view);
  }

OK, so the first thing is we're setting up an event listener. Remember, we just added a button that has been marked up with the above data attribute. When it's activated, it calls removeUserPreference.

Before we get to that, we get to removeModel. This is a utility function that performs a removal of the model from the collection. This is kept separate from the other function that performs the visual aspect of removal (ie the confirmation). It is only called, in our code, when the user confirms their wish.

Which brings us to the main function, removeUserPreference. We start by intercepting the event (the button clicking) and preventing any default actions. Then we create a new view — our confirmation window — and pass it a bunch of options to define it. You can see that we call back to the utility function on approval.

The final part is some code to show the view in the modal. I could write a lot about how this works and what's going on, or I could just point you to another post I wrote a while ago about the very subject.

Save, Deploy and Test

And that's it! We're now ready for our final save, deploy and test.

Once it's up, visit the page and perform a delete. Do you get something like this?

Furthermore, does the page update and remove the recently deleted record? If so, great success!

Final Thoughts

This one was a bit of a long one, but we added a lot of functionality. We moved on from the basic service and service controller we had before, to a fully-functional series of web pages that allow users to create, read, update and delete preferences on the website.

We added some small bits of a viable user interface/experience in the form of a modal that appears when a user goes to delete a record, but it's far from perfect.

In the next part (coming soon), we will round out the functionality by adding in the rest of stuff a user would expect to see: ranging from breadcrumbs to buttons, menu links to styling with Sass.

If you're having difficulty with the module, or want to compare code, take a look at UserPreferences@1.0.0-part3.zip, which is my code — note that this does not include the distro file.