import app from 'cpb-api';
import EJSON from 'ejson';
import fs from 'fs';
import * as pluralize from 'pluralize';
import process from 'process';
import util from 'util';
import { uuid, uuidFromUrl } from './uuid.js';
/**
* @module cpb-common
* @description
* ### Common (shared) functions
*/
export { uuid, uuidFromUrl };
export const { plural } = pluralize.default;
export const DEBUG = isDebug();
/**
* ### Split source array into chunks
* @param {*} whole - source array
* @param {?number} [size=10] - chunk size
* @return {*} result - array with chunks
*/
export const makeChunks = (whole, size = 10) => {
if (!whole || !whole.length) {
return [];
}
if (!size || typeof size !== 'number' || isNaN(size)) {
// throw new TypeError( '!size' );
console.error('makeChunks size is not a number');
size = 10;
}
const result = [];
let temp = [];
while (whole.length) {
if (temp.length < size) {
temp.push(whole.shift());
} else {
result.push(temp);
temp = [];
}
}
result.push(temp);
return result;
};
/**
* ### GENERATE DELIMITER LINE
* @static
* @property {string} prop.char - char for line
* @property {number} prop.length - line length
* @property {number|string} prop.breaks - end of line breaks count
* @returns {string} line - delimiter line
* @example
* line(); //
* =>'\n------------------------------------------------------------\n' line({chr:'+', length:10});
* // => '\n++++++++++\n' line({chr:'>+', length:10, breaks: 5}); // =>
* '\n\n\n\n\n>+>+>+>+>+>+>+>+>+>+\n\n\n\n\n'
*/
export function line({ chr = '-', length = 72, breaks = 0 } = {}) {
const { l, b, c } = isObject(arguments[0])
? { l: length, c: chr, b: breaks }
: {
l: arguments[1],
c: arguments[0],
b: arguments[2],
},
rpt = String.prototype.repeat,
noBreaks = isNaN(parseInt(b)) ? 0 : parseInt(b),
br = rpt.call('\n', noBreaks);
return ['\n', rpt.call(c, l), br].join('');
}
/**
* ### true if debug mode enabled
* @returns {boolean} isDebug
*/
export function isDebug() {
return typeof process.env.DEBUG === 'undefined' ? false : isTrue(process.env.DEBUG);
}
/**
* ### evaluate to boolean true value
* @param {*} d
* @returns {boolean}
*/
export function isTrue(d) {
return [true, 'true', 'TRUE', 1, '1'].indexOf(d) !== -1;
}
/**
* ### Get current app release:version
* @returns {string} [release= `${process.env.npm_package_name}:${process.env.npm_package_version}'] - release info
*/
export function getRelease() {
const pjson = JSON.parse(fs.readFileSync('./package.json').toString());
return `${pjson.name}:${pjson.version}`;
}
/**
* ### APP RELEASE VERSION
* @type {string}
*/
export const release = getRelease();
/**
* ### Get current timestamp in YYMMDDHMSS format
* @param {?date} [date=new Date()] - date to create the timestamp from
* @returns {string} ts - current timestamp
*/
export const timestamp = date => (date || new Date().toISOString()).toString().replace('T', ' ').replace('Z', '').replace('+', '.');
/**
* ### Check if Object
* @param {*} obj - anything
* @returns {boolean} isObject - true if object; false otherwise
*/
export function isObject(obj) {
return !!obj && typeof obj === 'object' && !isIterable(obj);
}
/**
* ### checks for Iterability
* @param {*} d - anything
* @return {boolean} isIterable
*/
export const isIterable = d => (!d ? false : typeof d[Symbol.iterator] === 'function');
/**
* ### check whether the given param is an array
* @param {*} obj
* @returns {boolean}
*/
export function isArray(obj) {
return !!obj && typeof obj === 'object' && isIterable(obj);
}
/**
* ### Convert data to JSONL string
* @param {object|string|Array<object|string>} d
* @return {string} JSONL - JSONL formatted output
*/
export function toJSONL(d) {
if (d.map) {
return d.map(JSON.stringify).join('\n');
}
if (isObject(d)) {
const data = [];
const [, b] = Object.entries(d);
if (isObject(b)) {
for (const [, v] of Object.entries(d)) data.push(v);
return data.map(JSON.stringify).join('\n');
}
return JSON.stringify(d);
}
}
/**
* ### parse JSONL from the local file
* @param {string} fileName - name of the file to parse data from
* @param {string} [name=fileName] - filename
* @param {boolean} [generateImportedAt=true] - add DATETIME::IMPORTED_AT to the record
* @param {boolean} [generateUuid=true] - add uuidv5::uuid to the record based upon object data
* @param {string[]} generateUuidExclude -list of attributes to be excluded from uuid hashing
* @returns {object[]}
*/
export function parseJSONL({ fileName, name = fileName, generateImportedAt = true, generateUuid = true, generateUuidExclude = [] }) {
if (!name) throw new TypeError('!name');
const raw = fs.readFileSync(name).toString().split('@type').join('_type'),
data = [];
for (const record of raw.split('\n')) {
if (!record || !record.length) continue;
const entry = JSON.parse(record);
entry.filename = name;
if (generateUuid) entry.uuid = uuid({ ...sortObjectAttributes(entry) }, [...generateUuidExclude, 'filename', 'imported_at', 'uuid']);
if (generateImportedAt) entry.imported_at = timestamp();
data.push(entry);
}
return data;
}
/**
* Prints Message with Formatting
*/
export function title() {
const delim = '\n------------------------------------------------------------------------------\n';
console.info(...arguments, delim);
}
/**
* ### check if the last character of string is the forward slash '/'
* @param {string|*} str
* @return {boolean}
*/
export function hasTrailingSlash(str) {
return typeof str === 'string' && str.lastIndexOf('/') === str.length - 1;
}
/**
* ### remove last character from string if it is a forward slash
* @param {string} str
* @returns {string|*} str - chopped string
*/
export function removeTrailingSlash(str) {
if (hasTrailingSlash(str)) return str.substring(0, str.length - 1);
return str;
}
/**
* ATTACHES EXCEPTIONS HANDLERS TO THE CURRENT NODE PROCESS
*/
export const attachProcessHandlers = () => {
const handlers = {
warning(warning) {
title('process.onWarning', { warning }, true);
},
uncaughtException(error) {
title('process:uncaughtException', { error }, true);
process.exit(1);
},
unhandledRejection(reason, promise) {
title('process.onUnhandledRejection', { reason, promise }, true);
process.exit(1);
},
uncaughtExceptionMonitor(error, origin) {
return title('process:uncaughtExceptionMonitor', { error, origin }, true);
},
};
process.title = getRelease();
// title( `attachProcessHandlers:${ process.title }` );
for (const event of Object.keys(handlers)) {
process.on(event, handlers[event]);
}
};
/**
* ### Sort object attributes alphabetically Ascending
* @param {!object} o - object to sort
* @param {?*} [reverse] - reverse the sort order
* @returns {object} sorted - new sorted object
*/
export function sortObjectAttributes(o, reverse) {
if (!isObject(o)) throw new TypeError('o is not an object');
const sorted = {},
keys = reverse ? Object.keys(o).sort().reverse() : Object.keys(o).sort();
for (const key of keys) sorted[key] = isObject(o[key]) ? sortObjectAttributes(o[key]) : o[key];
return sorted;
}
/**
* ### LOGS ALL SUPPLIED ARGUMENTS AS ERROR IN FORMATTED JSON
*/
export function error() {
title('ERROR HAS OCCURRED');
console.error(JSON.stringify({ ...arguments }, null, 2), line('#', 72, 1));
}
/**
* ### util.inspect settings for printing deep json structures
* @param {?string} [nl='\n'] - new line ending character
* @param {?Array()} [out=[]] - shared output array
* @param {?object} [options] - util.inspect.options
* @param {number} [options.depth=6]
* @param {boolean} [options.colors=true]
* @param {boolean} [options.sorted=true]
* @param {boolean} [options.getters=true]
* @param {boolean} [options.compact=true]
* @param {boolean} [options.showHidden=true]
* @returns {string} result - text for outputting
*/
export function inspect({
nl = '\n',
out = [],
options = {
depth: 6,
colors: true,
sorted: true,
getters: true,
compact: true,
showHidden: false,
},
} = {}) {
for (const arg of [...arguments]) {
out.push(util.inspect(arg, options));
}
return out.join(nl);
}
//
///**
// * ### performs get request and returns response body in the specified format
// * @async
// * @param {!string} url
// * @param request
// * @return {Promise<*>}
// */
//export async function get(url, { request } = {}) {
// if (!url) {
// throw new TypeError('!url');
// }
// // if (!format) {
// // throw new TypeError('!format');
// // }
// // if (!method) {
// // throw new TypeError('!method');
// // }
// let response;
// console.info(`[get] ${url}`);
//
// try {
// const format = 'jsonl',
// method = 'GET',
// options = { url, format, method, ...request };
// response = await axios.get(url, options);
// if (response.statusText !== 'OK') {
// throw new Error(stringify(response));
// }
// } catch (error) {
// title('[ERROR] get:', { url, error }, 1);
// throw new Error(error);
// }
//
// return response.data;
//}
///**
// * ### check if the object has nested objects in its properties
// * @param {object} o - object to be checked
// * @return {boolean} [hasNestedProps=true|false] - presence flag
// */
//export const hasNestedProps = o => {
// for (const key of Object.keys(o)) if (isObject(o[key])) return true;
// return false;
//};
/**
* ### converts all project attributes to lowercase values
* @returns {object}
* @param link
*/
export const lowercaseProperties = link => {
const norm = {};
for (const prop of Object.keys(link)) {
norm[prop.toLowerCase()] = link[prop];
}
return norm;
};
/**
* ### Get unique array values
* @returns {string[]}
* @example
* uniq(1,2,3,2,2,2) // [ '1', '2', '3' ]
*/
export function uniq() {
const r = {},
args = [...arguments];
let arr;
// console.debug({args});
while ((arr = args.shift())) {
if (!isArray(arr)) arr = [arr];
for (const k of arr) r[k] = arr[k];
}
return Object.keys(r);
}
/**
* ### EJSON Stringify Object
* @param {object|*} data
* @param {?boolean} [canonical=true]
* @param {?number} [indent=0] - formatter indent. Defaults to no indents (one object per line)
* @return {*}
*/
export function stringifyJSON(data, { canonical = true, indent = app.get('json spaces') || 0 } = { canonical: true, indent: 0 }) {
return EJSON.stringify(data, { canonical, indent });
}
/**
*### cast string or integer values to boolean
* @param {*} f - value to convert
* @param {boolean|number} [defaultValue=0] - default value
* @returns {boolean}
*/
export function getBinaryFlagValue(f, defaultValue = 0) {
const falseValues = ['0', 'false', 'nan', '-infinity', 'undefined', 'null'];
if (typeof f === 'undefined') return getBinaryFlagValue(defaultValue);
else return !(!!f.toString && falseValues.includes(f.toString().toLowerCase()));
}
/**
* ### Create Datastore Query Filter Array from Object
* @param {object} filterObject
* @returns {Array(string, unknown)}
*/
export function createQueryFilter(filterObject) {
if (!isObject(filterObject)) throw new TypeError('!filterObject');
return Object.entries(filterObject).filter(([k, v]) => {
if (typeof v !== 'undefined') return [k, v];
});
}
/**
* ### Pseudo-random number generator
* @return {number}
*/
export const randomNumber = () => Math.round(Math.random(5) * 1000);