Expand your SuiteScript skills in the Sustainable SuiteScript community for NetSuite developers.

Streaming to a File Line-by-Line in NetSuite with SuiteScript

Created: February 6, 2024

This is a sample chapter adapted from the "Managing Files with SuiteScript 2.1" Cookbook, part of the SuiteScript by Example series.

NetSuite's N/file module enables us to manipulate files within the File Cabinet in a variety of ways. Generally, we're usually copying, receiving, or writing the entire contents all at once.

Other times, it's more convenient to work one line at a time. Say we want to generate a CSV from some data we've retrieved from elsewhere. It could be from a search or an external system or any other source. It's much easier to work with a CSV one line at a time, and we can use NetSuite's streaming API to do so.

N/file is Server-Side Only

Before we can get to working with those files, there's a little setup work to do first. N/file can only be used in server-side scripts, meaning we can't drop code in the browser console and expect it to work.

Instead, we're going to build a Suitelet that will interact with our files for us.

We'll start by building a custom module file. This is where we'll be adding and changing the code from the upcoming examples:

/**
 * Custom module for executing N/file cookbook examples
 *
 * @NApiVersion 2.1
 * @NModuleScope SameAccount
 *
 * @author Eric T Grubaugh <eric@stoic.software> (https://stoic.software/)
 */
define(['N/file'], (f) => {
  // This is where our example code will go
})
  1. Create a folder in the File Cabinet at /SuiteScripts/file-cookbook/
  2. Upload the above source code into the new folder in a file named file-cookbook.js.
  3. From now on, I will refer to this file as the "file-cookbook module".
/**
 * Suitelet for testing File interactions
 *
 * @NApiVersion 2.1
 * @NModuleScope SameAccount
 * @NScriptType Suitelet
 *
 * @author Eric T Grubaugh <eric@stoic.software> (https://stoic.software/)
 */
define(['./file-cookbook', 'N/https'], function (fileCookbook, https) {
  /** @see https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4407987288.html */
  const onRequest = (context) => {
    log.audit({ title: context.request.method + ' request received' })

    // Ignore POST requests
    if (context.request.method !== https.Method.GET) {
      return
    }

    fileCookbook.writeFile(context.response)

    log.audit({ title: 'Request complete.' })
  }

  return { onRequest }
})

Next:

  1. Use the above source code to create a second file in the same folder as before.
  2. Use this second file to create a new Suitelet named File Interaction.
  3. Create a Deployment for the Suitelet; leave it in Testing status.
  4. On the Deployment, add a new Link in the Links sublist. Locate it somewhere accessible to the Role you'll be using to test the examples in this book. For me, using the Administrator Role, I chose Classic Center > Setup > Custom > File Interaction.

Suitelet Deployment settings

⚠️ If either of these code files is named differently or is not in the same folder, you will likely receive MODULE_NOT_FOUND errors when attempting to access the Suitelet.

This Suitelet is our test runner for working with files. Whenever we need to test one of our code examples, we access the link for the Suitelet in NetSuite's main navigation, and we can monitor the resulting Execution Logs on the Suitelet record.

For the remainder of this article, you should not need to make any modifications to the Suitelet or its source code.

Streaming Lines to a File

/**
 * Stream data to a file line-by-line
 *
 * @NApiVersion 2.1
 * @NModuleScope SameAccount
 *
 * @author Eric T Grubaugh <eric@stoic.software> (https://stoic.software/)
 */
define(['N/file'], (f) => {
  const WeatherData = [
    { date: '01/01/2020', high: 51, low: 19 },
    { date: '01/02/2020', high: 45, low: 27 },
    { date: '01/03/2020', high: 43, low: 20 },
    { date: '01/04/2020', high: 55, low: 22 },
    { date: '01/05/2020', high: 41, low: 26 },
    { date: '01/06/2020', high: 43, low: 30 },
    { date: '01/07/2020', high: 57, low: 31 },
    { date: '01/08/2020', high: 55, low: 23 },
    { date: '01/09/2020', high: 42, low: 26 },
    { date: '01/10/2020', high: 31, low: 13 }
  ]

  const writeFile = (response) => {
    const weatherFile = f.create({
      name: 'weather.csv',
      fileType: f.Type.CSV,
      description: 'Stream data to a file, line by line',
      folder: -15
    })

    const lines = WeatherData.map((w) =>
      [w.date, w.low, w.high].join(',')
    )

    lines.forEach((w) => {
      weatherFile.appendLine({ value: w })
    })

    weatherFile.save()

    response.write({ output: weatherFile.getContents() })
  }

  return { writeFile }
})

Create the File

We create the File using the file.create method, except we do not specify any contents:

const weatherFile = f.create({
  name: 'weather.csv',
  fileType: f.Type.CSV,
  description: 'Stream data to a file, line by line',
  folder: -15
})

Prepare contents

As stated previously, our data could be from any imaginable source; for this example I've created it statically within our script.

const WeatherData = [
  { date: '01/01/2020', high: 51, low: 19 },
  { date: '01/02/2020', high: 45, low: 27 },
  { date: '01/03/2020', high: 43, low: 20 },
  { date: '01/04/2020', high: 55, low: 22 },
  { date: '01/05/2020', high: 41, low: 26 },
  { date: '01/06/2020', high: 43, low: 30 },
  { date: '01/07/2020', high: 57, low: 31 },
  { date: '01/08/2020', high: 55, low: 23 },
  { date: '01/09/2020', high: 42, low: 26 },
  { date: '01/10/2020', high: 31, low: 13 }
]

From there, we map over the Objects, turning each into a comma-separated string of date,low,high.

const lines = WeatherData.map((w) =>
  [w.date, w.low, w.high].join(',')
)

This gives us all the lines of our CSV in an Array, each element of the Array represents one line of the file.

Stream contents to file, line-by-line

File instances have an appendLine method for writing data into them one line at a time.

We already have our Array of lines, so we iterate over the Array with forEach, and invoke appendLine() for each element in the Array.

lines.forEach((w) => {
  weatherFile.appendLine({ value: w })
})

Note that appendLine() can only be used on Text or CSV file types, and each line can be no more than 10MB. It is also not limited to new files. We can use this method to load an existing file and append data to the end of it without disturbing the original contents.

Once all the lines are written, we use the save method to store the File in the File Cabinet, and we write the contents to our Suitelet response.

weatherFile.save()

response.write({ output: weatherFile.getContents() })

Refreshing the Suitelet page should now display our newly created CSV data.