Skip to content

Instantly share code, notes, and snippets.

@bleonard
Created August 22, 2025 16:40
Show Gist options
  • Select an option

  • Save bleonard/9e415db628daf4f8ba9443eb2303dfe8 to your computer and use it in GitHub Desktop.

Select an option

Save bleonard/9e415db628daf4f8ba9443eb2303dfe8 to your computer and use it in GitHub Desktop.
/**
*@NApiVersion 2.x
*@NScriptType Restlet
*/
// eslint-disable-next-line no-undef
define(["N/record", "N/log", "N/search"], function (record, log, search) {
// It does this thing where it's "line 1" object key instead of an array. make it an array.
function normalize_sublists(sublists) {
var out = {};
// get all sublist names without for...in
var subNames = Object.keys(sublists || {});
for (var i = 0; i < subNames.length; i++) {
var name = subNames[i];
var block = sublists[name];
// only treat plain objects (not arrays) as sublists
if (block && typeof block === "object" && !Array.isArray(block)) {
var linesArr = [];
// extract keys in block
var blkKeys = Object.keys(block);
// collect just the "line N" entries
for (var j = 0; j < blkKeys.length; j++) {
var k = blkKeys[j];
if (/^line\s+\d+$/.test(k)) {
linesArr.push({
idx: parseInt(k.replace("line ", ""), 10),
value: block[k],
});
}
}
// sort by numeric index, then strip out the idx wrapper
linesArr.sort(function (a, b) {
return a.idx - b.idx;
});
var flat = [];
// eslint-disable-next-line no-redeclare
for (var j = 0; j < linesArr.length; j++) {
flat.push(linesArr[j].value);
}
out[name] = flat;
} else {
// leave arrays or primitives untouched
out[name] = block;
}
}
return out;
}
function load_row(input) {
log.debug("load_row", "Starting with input: " + JSON.stringify(input));
if (!input.id) {
log.error("load_row", "No id provided in input");
throw new Error("No id provided");
}
if (!input.recordtype) {
log.error("load_row", "No recordtype provided in input");
throw new Error("No recordtype provided");
}
var loadedRecord = record.load({
type: input.recordtype,
id: input.id,
isDynamic: true,
});
var loadColumns = input.load_columns;
if (loadColumns && ["all", "columns"].includes(input.load_key)) {
for (var i = 0; i < loadColumns.length; i++) {
var column = loadColumns[i];
var text = loadedRecord.getText({ fieldId: column });
if (text !== undefined) {
loadedRecord.load[column + "_text"] = text;
}
}
}
log.debug(
"load_row",
"Successfully loaded record type: " +
input.recordtype +
", id: " +
input.id
);
return loadedRecord;
}
function output_row_json(row) {
var out = JSON.parse(JSON.stringify(row));
if (out.sublists) {
log.debug("output_row_json ", "Normalizing sublists");
out.sublists = normalize_sublists(out.sublists);
} else {
log.debug(
"output_row_json",
"No sublists to normalize: " + JSON.stringify(Object.keys(out))
);
}
return out;
}
function delete_row(row) {
log.debug(
"delete_row",
"Starting deletion for record type: " + row.type + ", id: " + row.id
);
record.delete({
type: row.type,
id: row.id,
});
log.audit(
"delete_row",
"Successfully deleted record type: " + row.type + ", id: " + row.id
);
}
function coerce_value(row, fieldId, value, state) {
log.debug(
"coerce_value",
"Coercing field: " +
fieldId +
", value: " +
JSON.stringify(value) +
" state: " +
JSON.stringify(state)
);
if (value === null || value === undefined) {
log.debug("coerce_value", "Value is null/undefined, returning null");
return null;
}
var field = null;
try {
if (state["selected_sublist"]) {
field = row.getSublistField({
sublistId: state["selected_sublist"],
fieldId: fieldId,
line: state["selected_sublist_line_num"] || 0,
});
} else {
field = row.getField({ fieldId: fieldId });
}
} catch (e) {
log.error("coerce_value", "Error getting field: " + e.message);
throw e;
}
if (!field) {
var errorMsg =
"Field not found: " +
fieldId +
" in " +
row.type +
" state: " +
JSON.stringify(state);
log.error("coerce_value", errorMsg);
throw new Error(errorMsg);
}
// types
// text
// date
// rate
// datetime
// select
// checkbox
// currency
// integer
// float
// phone
// email
// url
var type = field.type;
log.debug("coerce_value", "Field type: " + type + " for field: " + fieldId);
if (type === "date" && typeof value === "string") {
var [m, d, y] = value.split("/");
var dateValue = new Date(Number(y), Number(m) - 1, Number(d));
log.debug(
"coerce_value",
"Converted date string to Date object: " + dateValue
);
return dateValue;
}
if (type === "datetime" && typeof value === "string") {
// iso format: e.g. 2020-09-20T13:45:00Z
var datetimeValue = new Date(value);
log.debug(
"coerce_value",
"Converted datetime string to Date object: " + datetimeValue
);
return datetimeValue;
}
return value;
}
function get_text_field(fieldId) {
// returns real field name
if (fieldId.slice(-5) === "_text") {
return fieldId.slice(0, -5);
}
return null;
}
function set_row_field(row, fldName, value) {
try {
log.debug(
"set_row_field",
"Set field: " + fldName + " = " + JSON.stringify(value)
);
var realName = get_text_field(fldName);
if (realName) {
row.setText(realName, value);
} else {
value = coerce_value(row, fldName, value, {});
row.setValue(fldName, value);
}
} catch (e) {
log.error(
"set_row_field",
"Error setting field " + fldName + ": " + e.message
);
throw e;
}
}
function set_subrecords(row, subrecords, state) {
log.debug(
"set_subrecords",
"Starting with subrecords: " + JSON.stringify(Object.keys(subrecords))
);
for (var subrecordId in subrecords) {
var subrecord = subrecords[subrecordId];
var mode = subrecord.mode;
log.debug(
"set_subrecords",
"Processing subrecord: " + subrecordId + ", mode: " + mode
);
if (mode !== "set") {
var errorMsg1 =
"Unsupported subrecord mode (" + subrecordId + "): " + subrecord.mode;
log.error("set_subrecords", errorMsg1);
throw new Error(errorMsg1);
}
for (var i = 0; i < subrecord.items.length; i++) {
var item = subrecord.items[i];
log.debug(
"set_subrecords",
"Processing subrecord item: " + i + " for subrecord: " + subrecordId
);
var my_subrecord = null;
try {
if (state["selected_sublist"]) {
// in a sublist
my_subrecord = row.getCurrentSublistSubrecord({
sublistId: state["selected_sublist"],
fieldId: subrecordId,
});
} else {
// not in a sublist
my_subrecord = row.getSubrecord({
fieldId: subrecordId,
});
}
} catch (e) {
log.error("set_subrecords", "Error getting subrecord: " + e.message);
throw e;
}
if (!my_subrecord) {
var errorMsg2 = "Subrecord not found: " + subrecordId;
log.error("set_subrecords", errorMsg2);
throw new Error(errorMsg2);
}
var fields = item.fields || {};
log.debug(
"set_subrecords",
"Setting data for subrecord: " + subrecordId
);
for (var fieldId in fields) {
var value = fields[fieldId];
set_row_field(my_subrecord, fieldId, value);
}
}
}
log.debug("set_subrecords", "Completed processing all subrecords");
}
function set_sublists(row, sublists, state) {
log.debug(
"set_sublists",
"Starting with sublists: " + JSON.stringify(Object.keys(sublists))
);
for (var sublistId in sublists) {
state["selected_sublist"] = sublistId;
var sublist = sublists[sublistId];
log.debug(
"set_sublists",
"Sublist: " + sublistId + " = " + JSON.stringify(sublist)
);
var mode = sublist.mode;
var lineCount = row.getLineCount({ sublistId: sublistId });
log.debug(
"set_sublists",
"Processing sublist: " +
sublistId +
", mode: " +
mode +
" lineCount: " +
lineCount
);
if (mode === "reset") {
// remove all existing lines backwards
var toRemove = row.getLineCount({ sublistId: sublistId });
log.debug(
"set_sublists",
"Resetting sublist, removing " + toRemove + " lines"
);
for (var i = toRemove - 1; i >= 0; i--) {
row.removeLine({ sublistId: sublistId, line: i });
}
lineCount = 0;
} else if (mode === "replace") {
log.debug(
"set_sublists",
"Replace mode, existing line count: " + lineCount
);
} else if (mode === "add") {
log.debug(
"set_sublists",
"Add mode, existing line count: " + lineCount
);
} else if (mode === "upsert") {
log.debug(
"set_sublists",
"Upsert mode, existing line count: " + lineCount
);
} else {
// eslint-disable-next-line no-redeclare
var errorMsg = "Unsupported sublist mode (" + sublistId + "): " + mode;
log.error("set_sublists", errorMsg);
throw new Error(errorMsg);
}
// eslint-disable-next-line no-redeclare
for (var i = 0; i < sublist.items.length; i++) {
var item = sublist.items[i];
log.debug(
"set_sublists",
"Processing sublist item: " + i + " for sublist: " + sublistId
);
var sublistLineNum = 0;
try {
if (mode === "upsert") {
// upsert is a hash of fields and values
// it needs to match each
var lookup = item.upsert || item.delete; // one of the other
if (!lookup) {
// eslint-disable-next-line no-redeclare
var errorMsg =
"No lookup found for sublist item upsert: " +
sublistId +
" " +
JSON.stringify(item);
log.error("set_sublists", errorMsg);
throw new Error(errorMsg);
}
var foundNum = -1;
for (var fieldId in lookup) {
var value = lookup[fieldId];
// can't coerce here because it will match on the wrong line
var lineNumber = row.findSublistLineWithValue({
sublistId: sublistId,
fieldId: fieldId,
value: value,
});
if (lineNumber < 0) {
foundNum = -1;
break; // need a new one
} else if (foundNum < 0) {
foundNum = lineNumber;
} else if (foundNum !== lineNumber) {
// second one didn't match the first
foundNum = -1;
break; // need a new one
} else {
// they were on the same line
}
}
if (item.upsert) {
// adding or updating
if (foundNum < 0) {
log.debug("set_sublists", "Selected new line: no upsert match");
row.selectNewLine({ sublistId: sublistId });
sublistLineNum = 0;
} else {
log.debug(
"set_sublists",
"Selected existing line upsert match: " + foundNum
);
row.selectLine({ sublistId: sublistId, line: foundNum });
sublistLineNum = foundNum;
}
} else if (item.delete) {
// deleting it
if (foundNum < 0) {
log.debug(
"set_sublists",
"Deleting sublist line: but no upsert match"
);
} else {
log.debug("set_sublists", "Deleting sublist line: " + foundNum);
row.removeLine({ sublistId: sublistId, line: foundNum });
}
continue;
} else {
// eslint-disable-next-line no-redeclare
var errorMsg =
"Unknown upsert action: " +
sublistId +
" " +
JSON.stringify(item);
log.error("set_sublists", errorMsg);
throw new Error(errorMsg);
}
} else if (mode === "replace" && i < lineCount) {
log.debug("set_sublists", "Selected existing line: " + i);
row.selectLine({ sublistId: sublistId, line: i });
sublistLineNum = i;
} else {
log.debug("set_sublists", "Selected new line");
row.selectNewLine({ sublistId: sublistId });
sublistLineNum = 0;
}
} catch (e) {
log.error("set_sublists", "Error selecting line: " + e.message);
throw e;
}
state["selected_sublist_line_num"] = sublistLineNum;
var fields = item.fields || {};
var subrecords = item.subrecords;
log.debug(
"set_sublists",
"Setting fields: " + JSON.stringify(Object.keys(fields))
);
// eslint-disable-next-line no-redeclare
for (var fieldId in fields) {
// eslint-disable-next-line no-redeclare
var value = fields[fieldId];
log.debug(
"set_sublists",
"Set sublist field: " + fieldId + " = " + JSON.stringify(value)
);
try {
var textFieldId = get_text_field(fieldId);
if (textFieldId) {
log.debug(
"set_sublists",
"Setting text field: " +
textFieldId +
" = " +
JSON.stringify(value)
);
row.setCurrentSublistText({
sublistId: sublistId,
fieldId: textFieldId,
text: value,
});
} else {
value = coerce_value(row, fieldId, value, state);
log.debug(
"set_sublists",
"Setting value field: " +
fieldId +
" = " +
JSON.stringify(value)
);
row.setCurrentSublistValue({
sublistId: sublistId,
fieldId: fieldId,
value: value,
});
}
} catch (e) {
log.error(
"set_sublists",
"Error setting sublist field " + fieldId + ": " + e.message
);
throw e;
}
}
if (subrecords) {
log.debug(
"set_sublists",
"Processing subrecords for sublist item: " + i
);
set_subrecords(row, subrecords, state);
}
try {
row.commitLine({ sublistId: sublistId });
log.debug("set_sublists", "Committed line for sublist: " + sublistId);
} catch (e) {
log.error("set_sublists", "Error committing line: " + e.message);
throw e;
}
delete state["selected_sublist_line_num"];
}
// remove any extra lines from the back
if (mode === "replace") {
var linesToRemove = lineCount - sublist.items.length;
if (linesToRemove > 0) {
log.debug(
"set_sublists",
"Removing " + linesToRemove + " extra lines from end"
);
// eslint-disable-next-line no-redeclare
for (var i = lineCount - 1; i >= sublist.items.length; i--) {
row.removeLine({ sublistId: sublistId, line: i });
}
}
}
delete state["selected_sublist"];
}
log.debug("set_sublists", "Completed processing all sublists");
}
function generateRandomString(length) {
var chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var result = "";
for (var i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
function process_mutation(row, mutation) {
var action = mutation.action;
log.debug(
"process_mutation",
"Processing mutation: " + JSON.stringify(mutation)
);
switch (action) {
case "set":
log.debug("process_mutation", "Mutation set field: " + mutation.field);
set_row_field(row, mutation.field, mutation.value);
break;
case "clear":
log.debug(
"process_mutation",
"Mutation clear field: " + mutation.field
);
set_row_field(row, mutation.field, null);
break;
case "unique":
log.debug(
"process_mutation",
"Mutation unique field: " + mutation.field
);
var fldName = mutation.field;
var max_length = mutation.field_length || 30;
var random_length = mutation.random_length || 6;
var name = row.getValue({ fieldId: fldName }) || "";
var randomString = generateRandomString(random_length);
if (name.length + randomString.length + 1 > max_length) {
name = name.substring(0, max_length - randomString.length - 2);
}
name = name + " " + randomString;
set_row_field(row, fldName, name);
break;
default:
var errorMsg = "Unsupported mutation action: " + action;
log.error("process_mutation", errorMsg);
throw new Error(errorMsg);
}
}
function set_row_data(row, input) {
log.debug("set_row_data", "Starting with input: " + JSON.stringify(input));
var fields = input.fields || {};
var mutations = input.mutations;
var sublists = input.sublists;
var subrecords = input.subrecords;
log.debug(
"set_row_data",
"Processing " + Object.keys(fields).length + " fields"
);
for (var fldName in fields) {
var value = fields[fldName];
set_row_field(row, fldName, value);
}
if (mutations) {
log.debug(
"set_row_data",
"Processing " + mutations.items.length + " mutations"
);
for (var i = 0; i < mutations.items.length; i++) {
var mutation = mutations.items[i];
process_mutation(row, mutation);
}
}
if (sublists) {
log.debug("set_row_data", "Processing sublists");
set_sublists(row, sublists, {});
}
if (subrecords) {
log.debug("set_row_data", "Processing subrecords");
set_subrecords(row, subrecords, {});
}
log.debug("set_row_data", "Completed setting row data");
}
// function row_to_json(row) {
// log.debug("row_to_json", "Converting row to JSON");
// try {
// var jsonResult = JSON.stringify(row);
// log.debug("row_to_json", "Successfully converted row to JSON");
// return jsonResult;
// } catch (e) {
// log.error("row_to_json", "Error converting row to JSON: " + e.message);
// throw e;
// }
// }
// Get a standard NetSuite record
function _read(input) {
log.audit(
"_read",
"Starting GET operation with input: " + JSON.stringify(input)
);
var row = load_row(input);
log.audit(
"_read",
"Successfully completed GET operation for record type: " +
input.recordtype +
", id: " +
input.id
);
return output_row_json(row);
}
// Delete a standard NetSuite record
function _delete(input) {
log.audit(
"_delete",
"Starting DELETE operation with input: " + JSON.stringify(input)
);
try {
var row = load_row(input);
// TODO dependant records first
delete_row(row);
var result = JSON.stringify({ id: input.id });
log.audit(
"_delete",
"Successfully completed DELETE operation for record type: " +
input.recordtype +
", id: " +
input.id
);
return result;
} catch (e) {
log.error("_delete", "DELETE operation failed: " + e.message);
throw e;
}
}
function _search(input) {
log.debug(
"_search",
"Starting SEARCH operation with input: " + JSON.stringify(input)
);
var inputSearchOptions = input.search;
if (inputSearchOptions) {
log.audit("_search", "search options provided");
} else {
if (!input.recordtype) {
log.error("_search", "No recordtype or search provided in input");
throw new Error("No recordtype provided");
}
if (Array.isArray(input.ids) && input.ids.length === 0) {
// it causes an error if no ids are provided
log.audit("_search", "No ids provided, returning empty array");
return { items: [] };
}
if (!Array.isArray(input.ids)) {
log.error("_search", "No ids array or search provided in input");
throw new Error("No ids array provided");
}
log.audit("_search", "Creating list ids search options");
inputSearchOptions = {
type: input.recordtype,
filters: [["internalid", "anyof", input.ids]],
columns: input.search_columns || [],
};
}
log.debug(
"_search",
"Performing SEARCH operation with options: " +
JSON.stringify(inputSearchOptions)
);
// the search some how mutates the columns array, so we need to clone it
var loadColumns = (input.load_columns || []).slice();
log.debug("_search", "Load columns: " + JSON.stringify(loadColumns));
try {
var searchObj = search.create(inputSearchOptions);
var items = [];
searchObj.run().each(function (result) {
// has id, recordType, values
var item = JSON.parse(JSON.stringify(result));
log.debug("_search", "Item Result: " + JSON.stringify(item));
if (["getvalue"].indexOf(input.load_key) >= 0) {
log.debug("_search", "Getting values for record: " + item["id"]);
var load = {};
for (var i = 0; i < searchObj.columns.length; i++) {
var column = searchObj.columns[i];
var key = column.join
? column.join + "." + column.name
: column.name;
load[key] = result.getValue(column);
var text = result.getText(column);
if (text !== undefined) {
load[key + "_text"] = text;
}
}
item["load"] = load;
} else if (["all", "columns"].indexOf(input.load_key) >= 0) {
log.debug("_search", "Loading record: " + item["id"]);
var row = load_row({
id: item["id"],
recordtype: item["recordType"] || input.recordtype,
});
switch (input.load_key) {
case "all":
log.debug("_search", "Loaded all record: " + item["id"]);
item["load"] = output_row_json(row);
break;
case "columns":
log.debug("_search", "Loading columns for record: " + item["id"]);
var load = {}; // eslint-disable-line no-redeclare
// eslint-disable-next-line no-redeclare
for (var i = 0; i < loadColumns.length; i++) {
var columnName = loadColumns[i];
log.debug(
"_search",
"Loading column: " + columnName + " for record: " + item["id"]
);
load[columnName] = row.getValue({ fieldId: columnName });
log.debug(
"_search",
"Loaded column: " + columnName + " for record: " + item["id"]
);
var text = row.getText({ fieldId: columnName }); // eslint-disable-line no-redeclare
if (text !== undefined) {
load[columnName + "_text"] = text;
}
}
item["load"] = load;
break;
default:
throw new Error("Unsupported load key: " + input.load_key);
}
}
items.push(item);
return true; // Continue iteration
});
var out = { items: items };
log.audit(
"_search",
"Successfully completed SEARCH operation for record type: " +
input.recordtype +
", id: " +
input.id
);
return out;
} catch (e) {
log.error("_search", "SEARCH operation failed: " + e.message);
throw e;
}
}
function _create(input) {
log.audit(
"_create",
"Starting CREATE operation with input: " + JSON.stringify(input)
);
if (!input.recordtype) {
log.error("_create", "No recordtype provided in input");
throw new Error("No recordtype provided");
}
var row = record.create({
type: input.recordtype,
isDynamic: true,
});
log.debug("_create", "Created new record of type: " + input.recordtype);
set_row_data(row, input);
var savedRecord = row.save();
log.audit(
"_create",
"Successfully saved new record with id: " + savedRecord
);
log.audit(
"_create",
"Successfully completed CREATE operation for record type: " +
input.recordtype
);
return output_row_json(row);
}
// Upsert a NetSuite record from request param
function _update(input) {
log.audit(
"_update",
"Starting UPDATE operation with input: " + JSON.stringify(input)
);
var row = load_row(input);
set_row_data(row, input);
var savedRecord = row.save();
log.audit("_update", "Successfully saved record with id: " + savedRecord);
log.audit(
"_update",
"Successfully completed UPDATE operation for record type: " +
input.recordtype +
", id: " +
input.id
);
return output_row_json(row);
}
function _select(input) {
// outputs select options for a field
log.audit(
"_select",
"Starting SELECT operation with input: " + JSON.stringify(input)
);
if (!input.recordtype) {
log.error("_select", "No recordtype provided in input");
throw new Error("No recordtype provided");
}
if (!input.field) {
log.error("_select", "No field provided in input");
throw new Error("No field provided");
}
var row = null;
if (input.id) {
row = load_row({
recordtype: input.recordtype,
id: input.id,
});
} else {
row = record.create({
type: input.recordtype,
isDynamic: true,
});
}
// some have to set up the row to get valid options (input.fields)
set_row_data(row, input);
var field = row.getField({ fieldId: input.field });
var options = field.getSelectOptions();
var out = {
items: options,
};
return out;
}
function _router(context) {
var method = context.method;
var methods = {
read: _read,
search: _search,
create: _create,
update: _update,
delete: _delete,
select: _select,
};
if (methods[method]) {
try {
return methods[method](context.input || {});
} catch (e) {
log.error(method, method + " operation failed: " + e.message);
throw e;
}
} else {
log.error("_post", "Unknown method: " + method);
throw new Error("Unknown method: " + method);
}
}
// return the methods we want to expose
return {
post: _router,
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment