SuiteScript 2.x Map/Reduce Script Code Samples
Two map/reduce script samples are provided:
-
Counting Characters Example – counts how often each alphabet letter appears in a string. This basic script helps you understand how map/reduce scripts work.
-
Processing Invoices Example– handles invoices and includes error-handling logic.
These script samples use SuiteScript 2.x. A newer version, SuiteScript 2.1, is also available and supports new language features that are included in the ES2019 specification. You can write map/reduce scripts using either SuiteScript 2.0 or SuiteScript 2.1.
-
For help with writing scripts in SuiteScript 2.x, see SuiteScript 2.x Hello World and SuiteScript 2.x Entry Point Script Creation and Deployment.
-
For more information about SuiteScript versions and SuiteScript 2.1, see SuiteScript Versioning Guidelines and SuiteScript 2.1.
Counting Characters Example
The following sample is a basic map/reduce script. This sample isn't meant for real-world business use, but rather to show how map/reduce scripts work.
This script defines a hard-coded string. The script counts each alphabet letter's occurrences in the string and creates a results file. Check the script comments for details on how the system processes it.
/**
* @NApiVersion 2.x
* @NScriptType MapReduceScript
*/
define(['N/file'], function(file) {
// Define characters that should not be counted when the script performs its
// analysis of the text.
const PUNCTUATION_REGEXP = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#\$%&\(\)\*\+,\-\.\/:;<=>\?@\[\]\^_`\{\|\}~]/g;
// Use the getInputData function to return two strings (1) the quick brown fox and (2) jumps over the lazy dog.
function getInputData() {
return "the quick brown fox \njumps over the lazy dog.".split('\n');
}
// After the getInputData function is executed, the system creates the following
// key-value pairs:
//
// key: 0, value: 'the quick brown fox'
// key: 1, value: 'jumps over the lazy dog.'
// The map function is invoked one time for each key-value pair. Each time the
// function is invoked, the relevant key-value pair is made available through
// the context.key and context.value properties.
function map(context) {
// Create a loop that examines each character in the string. Exclude spaces
// and punctuation marks.
for (var i = 0; context.value && i < context.value.length; i++) {
if (context.value[i] !== ' ' && !PUNCTUATION_REGEXP.test(context.value[i])) {
// For each character, invoke the context.write() method. This method saves
// a new key-value pair. For the new key, save the character currently being
// examined by the loop. For each value, save the number 1.
context.write({
key: context.value[i],
value: 1
});
}
}
}
// After the map function has been invoked for the last time, the shuffle stage
// begins. In this stage, the system sorts the 35 key-value pairs that were saved
// by the map function during its two invocations. From those pairs, the shuffle
// stage creates a new set of key-value pairs, where the each key is unique. In
// this way, the number of key-value pairs is reduced to 25. For example, the map
// stage saved three instances of {'e','1'}. In place of those pairs, the shuffle
// stage creates one pair: {'e', ['1','1','1']}. These pairs are made available to
// the reduce stage through the context.key and context.values properties.
// The reduce function is invoked one time for each of the 25 key-value pairs
// provided.
function reduce(context) {
// Use the context.write() method to save a new key-value pair, where the new key
// equals the key currently being processed by the function. This key is a letter
// in the alphabet. Make the value equal to the length of the context.values array.
// This number represents the number of times the letter occurred in the original
// string.
context.write({
key: context.key,
value: context.values.length
});
}
// The summarize stage is a serial stage, so this function is invoked only one
// time.
function summarize(context) {
// Log details about the script's execution.
log.audit({
title: 'Usage units consumed',
details: context.usage
});
log.audit({
title: 'Concurrency',
details: context.concurrency
});
log.audit({
title: 'Number of yields',
details: context.yields
});
// Use the context object's output iterator to gather the key-value pairs saved
// at the end of the reduce stage. Also, tabulate the number of key-value pairs
// that were saved. This number represents the total number of unique letters
// used in the original string.
var text = '';
var totalKeysSaved = 0;
context.output.iterator().each(function(key, value) {
text += (key + ' ' + value + '\n');
totalKeysSaved++;
return true;
});
// Log details about the total number of pairs saved.
log.audit({
title: 'Unique number of letters used in string',
details: totalKeysSaved
});
// Use the N/file module to create a file that stores the reduce stage output,
// which you gathered by using the output iterator.
var fileObj = file.create({
name: 'letter_count_result.txt',
fileType: file.Type.PLAINTEXT,
contents: text
});
fileObj.folder = -15;
var fileId = fileObj.save();
log.audit({
title: 'Id of new file record',
details: fileId
});
}
// Link each entry point to the appropriate function.
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
The character limit for keys in map/reduce scripts (specifically, in mapContext or reduceContext objects) is reduced to 3,000 characters. You'll also get error messages if your key is too long or your value is over 10 MB. Keys over 3,000 characters will return a KEY_LENGTH_IS_OVER_3000_BYTES error. Values over 10 MB will return a VALUE_LENGTH_IS_OVER_10_MB error. For more information about map/reduce limits see Map/Reduce Governance.
If you have map/reduce scripts that use the mapContext.write(options) or reduceContext.write(options) methods, keep your key strings under 3,000 characters and value strings under 10 MB. Consider the potential length of any dynamically generated strings which may exceed these limits and avoid using keys, instead of values, to pass your data.
There's a 200 MB limit on data passed between stages in a single execution. Should persistent data exceed this limit, the script will require provisions for a data custom field or record to be used in either context.
Processing Invoices Example
The following example shows a sample script that processes invoices and contains logic to handle errors. This script does the following:
-
Find the customers associated with all open invoices.
-
Apply a location-based discount to each invoice.
-
Write each invoice to the reduce stage so it's grouped by customer.
-
Initialize a new CustomerPayment for each customer applied only to the invoices specified in the reduce values.
-
Create a custom record capturing the details of the records that were processed.
-
Send an email to administrators about any exceptions.
Before running this sample, create a custom record type with ID "customrecord_summary", and text fields with id "custrecord_time", "custrecord_usage", and "custrecord_yields".
Script Sample Prerequisites
-
In NetSuite, go to Customization > List, Records, & Fields > Record Types > New.
-
On the Custom Record Type page, enter a name.
-
In the ID field, enter "customrecord_summary".
-
Select Save.
-
From the Fields subtab, do the following:
-
Select New Field. Enter a label and set ID to "custrecord_time". Ensure that the Type field is set to Free-Form Text. Select Save & New.
-
Select New Field. Enter a label and set ID to "custrecord_usage". Ensure that the Type field is set to Free-Form Text. Select Save & New.
-
Select New Field. Enter a label and set ID to "custrecord_yields". Ensure that the Type field is set to Free-Form Text. Select Save.
-
/**
* @NApiVersion 2.x
* @NScriptType MapReduceScript
*/
define(['N/search', 'N/record', 'N/email', 'N/runtime', 'N/error'],
function(search, record, email, runtime, error)
{
function handleErrorAndSendNotification(e, stage)
{
log.error('Stage: ' + stage + ' failed', e);
var author = -5;
var recipients = 'notify@example.com';
var subject = 'Map/Reduce script ' + runtime.getCurrentScript().id + ' failed for stage: ' + stage;
var body = 'An error occurred with the following information:\n' +
'Error code: ' + e.name + '\n' +
'Error msg: ' + e.message;
email.send({
author: author,
recipients: recipients,
subject: subject,
body: body
});
}
function handleErrorIfAny(summary)
{
var inputSummary = summary.inputSummary;
var mapSummary = summary.mapSummary;
var reduceSummary = summary.reduceSummary;
if (inputSummary.error)
{
var e = error.create({
name: 'INPUT_STAGE_FAILED',
message: inputSummary.error
});
handleErrorAndSendNotification(e, 'getInputData');
}
handleErrorInStage('map', mapSummary);
handleErrorInStage('reduce', reduceSummary);
}
function handleErrorInStage(stage, summary)
{
var errorMsg = [];
summary.errors.iterator().each(function(key, value){
var msg = 'Failure to accept payment from customer id: ' + key + '. Error was: ' + JSON.parse(value).message + '\n';
errorMsg.push(msg);
return true;
});
if (errorMsg.length > 0)
{
var e = error.create({
name: 'RECORD_TRANSFORM_FAILED',
message: JSON.stringify(errorMsg)
});
handleErrorAndSendNotification(e, stage);
}
}
function createSummaryRecord(summary)
{
try
{
var seconds = summary.seconds;
var usage = summary.usage;
var yields = summary.yields;
var rec = record.create({
type: 'customrecord_summary',
});
rec.setValue({
fieldId : 'name',
value: 'Summary for M/R script: ' + runtime.getCurrentScript().id
});
rec.setValue({
fieldId: 'custrecord_time',
value: seconds
});
rec.setValue({
fieldId: 'custrecord_usage',
value: usage
});
rec.setValue({
fieldId: 'custrecord_yields',
value: yields
});
rec.save();
}
catch(e)
{
handleErrorAndSendNotification(e, 'summarize');
}
}
function applyLocationDiscountToInvoice(recordId)
{
var invoice = record.load({
type: record.Type.INVOICE,
id: recordId,
isDynamic: true
});
var location = invoice.getText({
fieldId: 'location'
});
var discount;
if (location === 'East Coast')
discount = 'Eight Percent';
else if (location === 'West Coast')
discount = 'Five Percent';
else if (location === 'United Kingdom')
discount = 'Nine Percent';
else
discount = '';
invoice.setText({
fieldId: 'discountitem',
text: discount,
ignoreFieldChange : false
});
log.debug(recordId + ' has been updated with location-based discount.');
invoice.save();
}
function getInputData()
{
return search.create({
type: record.Type.INVOICE,
filters: [['status', search.Operator.IS, 'open']],
columns: ['entity'],
title: 'Open Invoice Search'
});
}
function map(context)
{
var searchResult = JSON.parse(context.value);
var invoiceId = searchResult.id;
var entityId = searchResult.values.entity.value;
applyLocationDiscountToInvoice(invoiceId);
context.write({
key: entityId,
value: invoiceId
});
}
function reduce(context)
{
var customerId = context.key;
var custPayment = record.transform({
fromType: record.Type.CUSTOMER,
fromId: customerId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: true
});
var lineCount = custPayment.getLineCount('apply');
for (var j = 0; j < lineCount; j++)
{
custPayment.selectLine({
sublistId: 'apply',
line: j
});
custPayment.setCurrentSublistValue({
sublistId: 'apply',
fieldId: 'apply',
value: true
});
}
var custPaymentId = custPayment.save();
context.write({
key: custPaymentId
});
}
function summarize(summary)
{
handleErrorIfAny(summary);
createSummaryRecord(summary);
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});