const fs = require('fs');
const crypto = require('crypto');
const common = require('./common');
/**
* A module containing helpful utilities to ease the use of xmtoolbox.<br><br>
*
* @module util
*/
/**
* @typedef module:util.CSVTOJSON_OPTIONS
* @type {Object}
* @property {number} headerRow Default: 0
*
* The line index where the header starts.
* @property {number} dataStartRow Default: 1
*
* The line index where the data starts.
*
* @property {string[]} headers default: undefined
*
* If no header row is included, include an array of string names for the columns.
* Included a name for all columns from left most column to right most needed column.
* If headers are not included it may be desired to set the dataStartRow option to start at index 0.
*/
/**
* Takes a path to a CSV file and returns a JSON object representing the file.
* Included to prevent the need for additional packages in most cases.
* If this doesn't work for you implement your own or use some of the other packages available.
* @param {string} path path to to CSV file.
* @param {module:util.CSVTOJSON_OPTIONS} options options object to allow some flexibility when converting CSV files to JSON.
* @returns {Object} JSON Object representation of the CSV data
*/
async function CsvToJsonFromFile(path, options) {
const data = await fs.promises.readFile(path, 'utf8');
return CsvToJson(data, options);
}
/**
* Takes a string of data in CSV format and returns a JSON object representing the data.
* Included to prevent the need for additional packages in most cases.
* If this doesn't work for you implement your own or use some of the other packages available.
* @param {string} data the CSV data.
* @param {module:util.CSVTOJSON_OPTIONS} options options object to allow some flexibility when converting CSV files to JSON.
* @returns {Object} JSON Object representation of the CSV data
*/
function CsvToJson(text, options = {}) {
let p = '';
let row = [''];
let lines = [row];
let i = 0;
let r = 0;
let toggle = true;
const delimiter = options.delimiter || ',';
//parse text and look for encoded commas.
for (let character of text) {
if (character === '"') {
if (toggle && character === p) row[i] += character;
toggle = !toggle;
} else if (character === delimiter && toggle) character = row[++i] = '';
else if (character === '\n' && toggle) {
if (p === '\r') row[i] = row[i].slice(0, -1);
row = lines[++r] = [(character = '')];
i = 0;
} else row[i] += character;
p = character;
}
const headerRow = options.headerRow || 0;
const dataStartRow = options.dataStartRow || 1;
//trim header names.
let headers = options.headers || lines[headerRow];
headers = headers.map(colName => colName.trim());
headers.filter(function (header) {
return header != null && header !== '';
});
return lines.slice(dataStartRow).map(line => {
const row = {};
for (let i = 0; i < headers.length; i++) {
row[headers[i]] = line[i];
}
return row;
});
}
function JsonToCsv(data, options = {}) {
// https://tools.ietf.org/html/rfc4180
// https://www.loc.gov/preservation/digital/formats/fdd/fdd000323.shtml
let columns;
if (options.columns) {
columns = options.columns;
} else {
columns = new Set();
data.map(row => {
Object.keys(row).forEach(column => columns.add(column));
});
}
columns = Array.from(columns);
const delimiter = options.delimiter || ',';
//add rows
let rows = data.map(d => {
return columns.map(column => (d[column] ? ParseCSV(d[column]) : '')).join(delimiter);
});
//add headers
const headers = columns.map(column => ParseCSV(column));
rows.unshift(headers);
return rows.join('\r\n');
}
//turn a string to a csv safe string
function ParseCSV(value) {
let text = value.toString();
text = text.replace(/"/g, '""');
if (text.indexOf('"') > -1 || text.indexOf(',') > -1) return '"' + text + '"';
return text;
}
/**
* POST request to xMatters Instance. Useful for sending data to a Workflow Flow.<br><br>
*
* Abides by the concurrency limit for the instance.<br><br>
*
* WARNING: The environment's readOnly option is not enforced when using this function.
* @param {module:environments.xMattersEnvironment} env The xmtoolbox representation of an xMatters instance.
* @param {string} api The api endpoint past the base URL. Example: '/api/xm/1/groups' or 'https://SUBDOMAIN.xmatters.com'
* @param {Object} json The object to include in the request body as json
* @param {Object} query A json object representing the query string parameters for this request.
*/
async function post(env, api, json, query) {
return common.request(env, api, query, { json, method: 'POST' }, 'Custom Request');
}
const EncryptToFile = (path, text, key, options) => {
const encrypted = Encrypt(text, key, options);
fs.writeFileSync(path, JSON.stringify(encrypted));
};
const DecryptFromFile = (path, key, options) => {
const encrypted = JSON.parse(fs.readFileSync(path, 'utf8'));
return Decrypt(encrypted, key, options);
};
const Encrypt = (text, key, options = {}) => {
if (key.length < 32) throw Error('The key must be at least 32 characters.');
const algorithm = options.algorithm || 'aes-256-gcm';
const bytes = options.bytes || 16;
const iv = crypto.randomBytes(bytes);
const cipher = crypto.createCipheriv(algorithm, key.substring(0, 32), iv);
const data = Buffer.concat([cipher.update(text), cipher.final()]);
const tag = cipher.getAuthTag();
const encrypted = {
iv: iv.toString('hex'),
data: data.toString('hex'),
tag: tag.toString('hex'),
};
return encrypted;
};
const Decrypt = (encrypted, key, options = {}) => {
if (key.length < 32) throw Error('The key must be at least 32 characters.');
const algorithm = options.algorithm || 'aes-256-gcm';
const { iv, data, tag } = encrypted;
const decipher = crypto.createDecipheriv(algorithm, key.substring(0, 32), Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(tag, 'hex'));
let decrypted = Buffer.concat([decipher.update(data, 'hex'), decipher.final()]);
return decrypted.toString();
};
const pick = (object, keys) => {
const _object = {};
if (!Array.isArray(keys)) keys = [keys];
keys.forEach(key => {
_object[key] = object[key];
});
return _object;
};
const find = (items, object) => {
const keys = Object.keys(object);
itemLoop: for (let i = 0; i < items.length; i++) {
const item = items[i];
keyLoop: for (let ki = 0; ki < keys.length; ki++) {
if (item[keys[ki]] !== object[keys[ki]]) continue itemLoop;
}
return item;
}
};
// supports string, number, date, functions, regex
// doesn't support buffers, array buffers, error objects, maps, sets, typed Arrays, symbols,
const isEqual = (object, source) => {
if (object === null || object === undefined || source === null || source === undefined) {
return object === source;
/* } else if (
typeof source !== typeof source || //not same type
Array.isArray(source) !== Array.isArray(object) //not both arrays
) {
console.log('isEqual 1');
return false; */
}
if (object.constructor !== source.constructor) {
return false;
}
if (object instanceof Function || object instanceof RegExp) {
//function or Regex
return object === source;
}
if (object === source || object.valueOf() === source.valueOf()) {
return true;
}
if (Array.isArray(object) && object.length !== source.length) {
return false;
}
if (object instanceof Date) {
//matches on valueof above. if here, they didn't match.
return false;
}
//if here, need to be object
if (!(object instanceof Object)) {
return false;
}
if (!(source instanceof Object)) {
return false;
}
//Object, check that they have matching key values for all and validate value for every key
const keys = Object.keys(object);
return (
Object.keys(source).every(function (ki) {
return keys.indexOf(ki) !== -1;
}) &&
keys.every(function (ki) {
return isEqual(object[ki], source[ki]);
})
);
};
/**
* Compares Objects for match, object can contain more than source
* @param {*} object
* @param {*} source
*/
const isMatch = (object, source) => {
if (!source) return true;
if (Array.isArray(object) && Array.isArray(source)) {
return source.every(sItem => object.some(oItem => isEqual(sItem, oItem)));
} else if (object instanceof Object && source instanceof Object) {
const keys = Object.keys(source);
return keys.every(key => isMatch(object[key], source[key]));
} else {
return isEqual(object, source);
}
};
/**
* if keys from source are undefined in object, they are set on the object and object is returned.
* @param {*} object
* @param {*} source
*/
const defaults = (object, source) => {
if (!object) return source;
if (!source) return object;
const keys = Object.keys(source);
keys.forEach(key => {
if (object[key] === undefined) object[key] = source[key];
});
return object;
};
const defaultsDeep = (object, source) => {
if (!object) return source;
if (!source) return object;
const keys = Object.keys(source);
keys.forEach(key => {
if (object[key] === undefined) {
object[key] = source[key];
} else if (
typeof source[key] === 'object' &&
source[key] !== null &&
typeof object[key] === 'object' &&
object[key] !== null
) {
object[key] = defaultsDeep(object[key], source[key]);
}
});
return object;
};
module.exports = {
CsvToJsonFromFile,
CsvToJsonFromData: CsvToJson,
CsvToJson,
JsonToCsv,
post,
Encrypt,
Decrypt,
EncryptToFile,
DecryptFromFile,
pick,
find,
isEqual,
isMatch,
defaults,
defaultsDeep,
};
Source