Skip to main content

Arrays and lists

Transforming lists

Many integrations can be summarized as a transformation of data from one format to another. For example, a JIRA ticket needs to be transformed to a Xurrent request, or the records of a system need to be exported to the data warehouse.

Many of these transformations work on arrays, or lists, of records, and AutomatorScript has excellent support for this. Not only are all the standard array methods available, including powerful ones like map, flatMap, filter, and reduce, but the language also has a handful of extension methods that make common data-shaping tasks shorter and clearer.

Filtering and transforming with filter, map and flatMap

If you're not familiar with functions like map, flatMap and filter, your first instinct, when confronted with the task to transform a list of objects, might be to write a loop with for and if. However, as the following example shows, this approach is often not the most readable or maintainable.

❌ Don't do this

Suppose that, from a list of notes, you want one combined text and all of their attachments in a single list, while skipping system notes. You could write a loop like this:

let text = '';
let attachments = [];

for (let note of notes ?? []) {
if (note.medium != 'system') {
if (text) {
text = text + "\n\n" + summarizeNote(note);
} else {
text = summarizeNote(note);
}
if (note.attachments && note.attachments.length) {
for (let att of note.attachments) {
attachments.push(att);
}
}
}
}

Although this works, it's hard to see what the loop is actually doing. It reads like a sequence of mechanical operations, and the intention of the code is not clear. Much of the code has nothing to do with the actual goal: two accumulator variables, a nested loop, and an if (text) check that exists only to avoid a leading separator.

✅ Do this instead

Filter out the system notes once, then derive both results from that list:

let nonSystemNotes = (notes ?? []).filter(note => note.medium != 'system');

let text = nonSystemNotes
.map(note => summarizeNote(note))
.join("\n\n");

let attachments = nonSystemNotes.flatMap(note => note.attachments ?? []);

Each line states a single intention:

  • filter keeps only the notes we care about.
  • map turns each note into its text, and join glues the pieces together with the separator between them. There's no special case for the first element.
  • flatMap maps each note to its attachments and flattens the results into one list, replacing the inner loop.

No accumulators, so nothing to initialize and nothing to mutate.

Aggregating with reduce

When the result of the list transformation is a single value that aggregates every element, for example a total, a count, or a summary object, reduce(fn, start) can often be used to express the transformation more clearly than a loop.

❌ Don't do this

Suppose you want the total amount of all orders that have been paid. With a loop, selecting and summing get tangled together: the if that picks the paid orders sits inside the same loop that maintains the running total.

let total = 0;
for (let order of orders) {
if (order.status == 'paid') {
total = total + order.amount;
}
}

Like the previous example, this code feels very "mechanical". When you would read it out loud, it might not be immediately clear what the intention is.

✅ Do this instead

Rewriting this example with filter and reduce gives the following:

let total = orders
.filter(order => order.status == 'paid')
.reduce((sum, order) => sum + order.amount, 0);

Even though the code is not actually shorter, its intention is clearer. The two parts of the transformation are clearly separated: take the orders that have been paid, and sum their amounts.

In a real integration, both steps are likely to be more complex than this. By extracting them into separate functions, the overall code will stay easy to understand:

let total = orders.filter(isPaid).reduce(sumAmounts, 0);

function isPaid(order) {
return order.status == 'paid';
}

function sumAmounts(sum, order) {
return sum + order.amount;
}

Extension methods

On top of the standard JavaScript methods, AutomatorScript adds several extension methods to the Array type.

Overview

MethodWhat it does
first()Returns the first element of the array, or null if it is empty.
last()Returns the last element of the array.
uniq()Returns a new array with duplicate values removed.
groupBy(key)Groups the elements into an object of arrays, keyed by a property.
indexBy(key)Indexes the elements into an object, keyed by a property (one element per key).
inSlicesOf(n)Splits the array into chunks of at most n elements.
pick(keys)Projects each object in the array down to only the given keys.
renameKeys(mapping)Renames the keys of each object in the array according to a mapping.
Array.wrap(value)Wraps a value in an array, unless it already is one.

Examples

Create a lookup table

Turning a flat list of records into a lookup by id:

let people = [
{ id: 3, name: 'Ada' },
{ id: 6, name: 'Alan' },
];

let byId = people.indexBy('id');
log('name of 6', byId[6].name); // 'Alan'

Batching records before sending them to an API:

for (let batch of records.inSlicesOf(50)) {
http_post('/bulk', batch, 'target');
}

Transforming records with pick and renameKeys

pick and renameKeys are especially useful together when the shape of the data you read differs from the shape you need to write. A typical case is exporting from Xurrent's REST API — which returns snake_case field names — into the CSV import format, which expects human-readable column headers.

For example, the configuration item REST resource returns records like { name, label, status, serial_nr, ... }, but the CI import expects the columns Name, Label, Status and Serial Number. Use pick to keep only the fields you need, then renameKeys to map them onto the import column names:

let cis = fetchFilter('cis/active', 'name=foo'); // Xurrent REST returns snake_case records

let rows = cis
.pick(['name', 'label', 'status', 'serial_nr'])
.renameKeys({
name: 'Name',
label: 'Label',
status: 'Status',
serial_nr: 'Serial Number',
});

let csv = arrayToCsvString(rows); // header row taken from the renamed keys
import_csv('cis', csv, 'wdc');

Because pick runs first, the objects passed to renameKeys — and therefore the CSV header — contain only the four columns you asked for, in that order.

Full reference

Each method, with its exact signature and more examples, is documented alongside the standard methods in the Built-in objects reference. The extension methods there are the ones that link to an anchor on that page rather than out to MDN.