UNPKG

ksmf

Version:

Modular Microframework for create minimalistic CLI/Web application or REST API

542 lines (510 loc) 18.1 kB
/** * @description Allow different data type conversions. * @module common */ class Utl { #config = { number: { decimals: ",", separator: "." }, defaultValue: "-", cleanValue: "" } get config() { return this.#config; } set config(options) { this.#config.number = options?.number || this.config.number; this.#config.defaultValue = options?.defaultValue ?? this.config.defaultValue; this.#config.cleanValue = options?.cleanValue ?? this.config.cleanValue; } /** * @description escape all characters used as symbols in a regular expression * @param {String|RegExp} str * @returns {String|RegExp} result */ escapeRegExp(str) { return (typeof str === "string" && str) ? str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : str; } /** * @description For legacy code. Replace all instances of a substring in a string, using a regular expression or search string. * @param {String} str * @param {String|RegExp} [find] A string to search for. * @param {String} [replace] A string containing the text to replace for every successful match of searchValue in this string. * @returns {String} */ replace(str, find, replace = "") { if (typeof (str) !== "string" || !str || !find) { return str; } return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace); } /** * @description get the marging value among two numbers * @param {String|Number} max * @param {String|Number} min * @param {Boolean} sign [false] * @returns {Number} */ getDelta(max, min, sign = false) { max = this.asNumber(max); min = this.asNumber(min); return ((!max && max !== 0) || (!min && min !== 0)) ? null : (sign ? max - min : Math.abs(max - min)); } /** * @description get the sign from a number * @param {String|Number} value * @param {Boolean} all * @returns {String} Sing */ getSign(value, all = true) { if (!value) { return ""; } value = typeof value === "string" ? parseFloat(value) : value; return value > 0 ? "+" : (all ? "-" : ""); } /** * @description get a valid value for boolean format * @param {String|Number|Boolean|Object|Array} value * @returns {Boolean} */ asBoolean(value, strict = true) { if (!value) { return false; } if (typeof (value) === "string") { value = value.trim(); value = !(!value || value.toLowerCase() === "false"); } if (strict && typeof (value) === "object") { if (Array.isArray(value)) { return value.length > 0; } value = (Object.keys(value).length + Object.getOwnPropertySymbols(value).length); } return !!value; } /** * @description check a valid value for number format * @param {String|Number} value * @returns {Boolean} */ isNumber(value) { return !this.isNaN(value) && value !== null && value !== undefined && value !== ""; } /** * @description check id the value is not a number * @param {String|Number} value * @returns {Boolean} */ isNaN(value) { let tmp = typeof value === "string" ? parseFloat(value) : value; return tmp != value; } /** * @description check a valid value * @param {String|Number} value * @param {String|Number} defaultValue * @returns {Boolean} */ isValid(value, defaultValue = null) { defaultValue = defaultValue ?? this.config.defaultValue; return ![defaultValue, "", " ", undefined, null].includes(value); } /** * @description get a valid value, avoiding a default and empty value * @param {String|Number} value * @param {String|Number} defaultValue * @returns {String|Number} */ clean(value, defaultValue = null) { defaultValue = defaultValue ?? this.config.defaultValue; if (typeof (value) === "string") { value = value.trim(); } return [defaultValue, "", undefined].includes(value) ? null : value; } /** * @description clear string * @param {String} value * @returns value */ clear(value) { if (typeof value === "string") { return value.replace(/\\r|\r|\n|\\n/g, "").replace(/(\s{2,})/g, " "); } return value; } /** * @description Transform a text string using Snake Case notation * @param {String} str * @returns {String} str */ toSnakeCase(str) { return str.replace(/([a-z])([A-Z])|[\s_]+/g, '$1_$2'); } /** * @description Transform a text string using Camel Case notation * @param {String} str * @returns {String} str */ toCamelCase(str) { return str.replace(/((_|\s)\w)/g, match => match[1].toUpperCase()); } /** * @description get a valid number * @param {String|Number} value * @param {Object} [config] * @returns {Number} Number */ asNumber(value, config) { config = config ?? this.config.number; value = typeof value === "string" ? this.asNumberFormat(value, config) : value; return this.isNumber(value) ? (typeof value === "string" ? parseFloat(value) : value) : null; } /** * @description convert string to number * @param {String} value * @param {Object} [config] * @param {String} [config.separator] * @param {String} [config.decimals] * @param {String} [config.force] * @param {String} [config.cleanValue] * @param {String} [config.defaultValue] * @returns {String} number */ asNumberFormat(value, config) { config = this.clone(this.config?.number, config || {}); if (typeof (value) === "string") { if (config.separator !== ".") { value = this.replace(value, config.separator); } else { const hasDecimals = value.includes(config.decimals); if (hasDecimals || (!hasDecimals && !config.force)) { value = this.replace(value, config.separator); } } if (config.decimals !== ".") { value = this.replace(value, config.decimals, "."); } value = this.replace(value, ",", "."); value = value === config.defaultValue ? config.cleanValue : value; } return value; } /** * @description add thousands separators * @param {String|Number} value * @param {Object} [options] * @returns {String} Number */ addNumberSeparator(value, options = null) { const { separator = ",", window = 3, integer = true } = options || {}; function set(value, window, separator) { return value.replace(new RegExp(`\\B(?=(\\d{${window}})+(?!\\d))`, 'g'), separator); } value = String(value); if (integer) { const parts = value.split("."); parts[0] = set(parts[0], window, separator); return parts.join("."); } else { return set(value, window, separator); } } /** * @description Get a decimal round based on the decimal amount * @param {String|Number} value * @param {Object} [config] * @param {String} [config.separator] * @param {String} [config.decimals] * @param {String} [config.force] * @param {String} [config.cleanValue] * @param {String} [config.defaultValue] * @param {String|Number} [config.format] * @param {String|Number} [config.window] * @returns {String|Number} */ round(value, config) { config = this.clone(this.config.number, typeof (config) === "number" ? { window: config } : (config || {})); let { window, format = Number } = config; value = typeof value === "string" ? this.asNumberFormat(value, config) : value; if (!this.isNumber(value)) { return null; } if (!this.isNumber(window)) { return value; } if (!(format instanceof Function)) { format = Number; } window = typeof window === "string" ? parseInt(window) : window; value = typeof value === "string" ? parseFloat(value) : value; return format((Math.round(value * Math.pow(10, window)) / Math.pow(10, window)).toFixed(window)); }; /** * @description Pads the current string/number with a given value (possibly repeated) so that the resulting string reaches a given length. * @param {String|Number} src * @param {Number} length * @param {String|Number} value * @param {String} defaults * @returns {String} Result */ padSrt(src, length = 3, value = '0', defaults = "") { src = String(src ?? ""); src = (src?.trim && src.trim()) ?? src; return src ? String(src).padStart(length, String(value)) : defaults; } /** * @description create a simple object map * @param {Array} lst * @param {Function} getIndex * @param {Function} getValue * @returns */ asMap(lst, getIndex, getValue) { return (lst?.reduce && lst)?.reduce((map, item, index) => { map[typeof (getIndex) === "function" ? getIndex(item, lst, index) : index] = typeof (getValue) === "function" ? getValue(item, lst, index) : item; return map; }, {}); } /** * @description deep clone object * @param {Object} obj * @param {Object} base * @returns {Object} result */ clone(obj, ...base) { const cryp = require('kscryp'); return Object.assign(this.asBoolean(obj, true) ? cryp?.decode(cryp?.encode(obj, "json"), "json") : {}, ...base); } /** * @description check if there is any difference between obj and base * @param {Object} obj * @param {Object} base * @returns {Boolean} different */ isDifferent(obj, base) { for (let i in base) { if (obj[i] !== base[i]) { return true; } } return false; } /** * @description truncate a string based on max of characters * @param {String} str * @param {Number} maxLength * @param {Object} options * @returns {String} value */ truncate(str, maxLength, { side = "end", ellipsis = "..." } = {}) { if (str.length > maxLength) { if (side === "start") { return ellipsis + str.slice(-(maxLength - ellipsis.length)); } else { return str.slice(0, maxLength - ellipsis.length) + ellipsis; } } return str; } /** * @description Provide modern functionality on older nodejs version that do not natively support it. */ polyfill() { const _this = this; // String if (!(String.prototype.replaceAll instanceof Function)) { Object.defineProperty(String.prototype, 'replaceAll', { value: function (...arg) { return _this.replace(this, ...arg); }, enumerable: false }); } // Array if (!(Array.prototype.at instanceof Function)) { Object.defineProperty(Array.prototype, 'at', { value: function (index) { if (index >= this.length) { return null; } if (index < 0) { index = this.length + index } return this[index]; }, enumerable: false }); } } /** * @description perform a smart search by key/RegEx in an object * @param {String} key * @param {Object} obj * @returns {*} Value */ search(key, obj, opt) { if (!obj) { return false; } if (obj[key]) { return obj[key]; } function getRegExp(str) { try { return new RegExp(str); } catch (e) { return null; } } let exp = getRegExp(key); let lst = Object.keys(obj); let ind = lst.find(item => { if (opt?.reverse) { let prp = getRegExp(item); return prp && prp.test(key); } else { return exp && exp.test(item); } }); return !ind ? false : obj[ind]; } /** * @description get all request params [POST, GET, Path] * @param {Object} req * @param {Object} option * @param {Array|Boolean} [option.clean] * @param {String|Function} [option.type] * @param {Object|Array|String} [option.key] * @returns {Object} params */ getFrom(req, option) { option = option || {}; req = req || {}; let act = type => typeof (type) === "function" ? type : (type && this["as" + type] ? this["as" + type] : null); // get data from a request object let opt = this.mixReq(req); if (req.files) { for (let i in req.files) { const file = req.files[i]; file && (opt[file.fieldname] = file.buffer?.toString ? file.buffer.toString("utf8") : file.buffer); } } // clean up reserved keywords if (!req.body) { req.query && typeof (req.query) === "object" && (delete opt["query"]); req.params && typeof (req.params) === "object" && (delete opt["params"]); } // perform filtering and type transformation let exe = act(option.type); if (option.key) { if (typeof (option.key) === "string") { return exe ? exe.apply(this, [opt[option.key], option]) : opt[option.key]; } if (Array.isArray(option.key)) { return option.key.reduce((store, i) => { store[i] = exe ? exe.apply(this, [opt[i], option]) : opt[i]; return store }, {}); } } if (option?.clean) { option.clean = option.clean && Array.isArray(option.clean) ? option.clean : [undefined, ""]; for (let i in opt) { if (option.clean.includes(opt[i])) { delete opt[i]; } if (option?.key && option.key[i]) { exe = act(option.key[i]) || exe; opt[i] = exe ? exe.apply(this, [opt[i], option]) : opt[i]; } } } return opt; } /** * @description avaluate any key from opt into src * @param {Object} opt * @param {String} key * @param {Object} org * @returns {Object} opt */ from(opt, key = "env", org = process) { if (!opt || !org || !org[key]) { return {}; } let src = org[key]; let tmp = { ...opt } for (let i in tmp) { if (src[tmp[i]]) { tmp[i] = src[tmp[i]]; } } return tmp; } /** * @description Define if child array is contained into a parent array * @param {Array} parent * @param {Array} child * @param {Function|null} [check] * @returns {Array} contained items */ contains(child, parent, check = null) { if (!child) { return []; } child = Array.isArray(child) ? child : [child]; if (!parent.length) { return []; } check = check instanceof Function ? check : ((item, lst) => lst.includes(item)); return child.filter(item => check(item, parent)); } /** * @description get all request params [POST, GET, Path] * @param {Object} req * @param {Object} req.params * @param {Object} req.query * @param {Object} req.body * @returns {Object} params */ mixReq(req) { return { ...req.params || {}, ...req.query || {}, ...req.body || req }; } static #instance; static self() { if (!this.#instance) { this.#instance = new Utl(); } return this.#instance; } /** * @description trasform objects using delegates * @param {*} obj * @param {Object} delegate * @returns {*} */ transform(obj, delegate = null, key = null) { if (!delegate) { return obj; } if (Array.isArray(obj)) { return obj.map((item, key) => this.transform(item, delegate, key)); } else if (obj !== null && typeof obj === 'object') { return Object.keys(obj).reduce((acc, key) => { let res = delegate?.onKey(key, obj[key]); if (res !== undefined) { acc[res || key] = this.transform(obj[key], delegate, key); } return acc; }, {}); } else { let resVal = delegate?.onVal(obj, key); return resVal !== undefined ? resVal : obj; } } } module.exports = Utl;