Source: common/index.js

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);