UNPKG

@qikdev/sdk

Version:

Promise based Javascript SDK

1,514 lines (1,284 loc) 38.6 kB
import _get from "lodash.get"; import _camelCase from "lodash.camelcase"; import { isBrowser, isNode } from "browser-or-node"; /////////////////////////////////////////////////////////////////////////////// /** * @name utils * @classdesc Utility helper functions — id, ids, hashing, parsing, cleaning, and other general-purpose helpers * @class */ const service = {}; //////////////////////////////////// const loadedExternalScripts = {}; /** * A function that dynamically include an external javascript resource * ensuring that it will only be included once * @alias utils.loadExternalScript * @param {String} url The url of the external script * @return {Promise} A promise that will be resolved once the script has been loaded * @example * * await sdk.utils.loadExternalScript('https://cdn.javascript.com/external/script.js'); */ service.loadExternalScript = function (url) { // If we already have a promise if (loadedExternalScripts[url]) { // return the existing promise return loadedExternalScripts[url]; } // Create a new promise const promise = (loadedExternalScripts[url] = new Promise(createNewScript)); function createNewScript(resolve, reject) { if (!document) { delete loadedExternalScripts[url]; return reject("document is undefined"); } ////////////////////////////////////// var script = document.createElement("script"); script.type = "text/javascript"; script.async = true; script.onload = function () { console.log("Loaded external script", url); return resolve(url); }; script.src = url; // Inject it into the document's <head> tag document.head.appendChild(script); } return promise; }; const loadedExternalStyles = {}; /** * A function that dynamically include an external css resource * ensuring that it will only be included once * @alias utils.loadExternalStyle * @param {String} url The url of the external css * @return {Promise} A promise that will be resolved once the css has been loaded * @example * * await sdk.utils.loadExternalStyle('https://cdn.css.com/external/style.css'); */ service.loadExternalStyle = function (url) { const promise = new Promise(createNewScript); function createNewScript(resolve, reject) { if (!document) { return reject("document is undefined"); } if (loadedExternalStyles[url]) { return resolve(url); } // Avoid duplicate scripts loadedExternalStyles[url] = true; ////////////////////////////////////// var script = document.createElement("link"); script.setAttribute("rel", "stylesheet"); script.setAttribute("type", "text/css"); script.setAttribute("href", url); script.onload = function () { console.log("Loaded external stylesheet", url); return resolve(url); }; // Inject it into the document's <head> tag document.head.appendChild(script); } return promise; }; /////////////////////////////////////////////////////////////////////////////// /** * A helper function for checking whether a value is truthy/falsy * @alias utils.exists * @param {Anything} value The value to check * @return {Boolean} whether the value is truthy * @example * * sdk.utils.exists('undefined'); // false * sdk.utils.exists([]); // true * sdk.utils.exists(''); // false * sdk.utils.exists(undefined); // false */ service.exists = function (value) { var isUndefinedOrNull; //////////////////////// if (Array.isArray(value)) { return true; } //////////////////////// if (value === 0) { return true; } //////////////////////// switch (typeof value) { case "undefined": case "null": isUndefinedOrNull = true; break; default: var string = String(value).toLowerCase(); switch (string) { case "": case "undefined": case "null": isUndefinedOrNull = true; break; } break; } return !!!isUndefinedOrNull; }; ////////////////////////////////// /** * A helper function for getting a javascript date object from some input * returns undefined if it can not be parsed * @alias utils.parseDate * @param {String|Number} input The value to parse as a date * @return {Date} the javascript date object * @example * * sdk.utils.parseDate(input); */ service.parseDate = function (input) { if (!input) { return; } if (input instanceof Date) { return input; } //Attempt to create a date var d = new Date(input); var isValid = d instanceof Date && !isNaN(d); if (isValid) { return d; } return; }; /** * A helper function for getting a full url string from some input * @alias utils.parseURL * @param {String} input The input value * @return {String} the fully parsed URL * @example * * sdk.utils.parseURL('google.com'); // Returns https://google.com * sdk.utils.parseURL('mailto:hello@qik.dev'); // Returns 'mailto:hello@qik.dev' * sdk.utils.parseURL('://hello.com'); // Returns '://hello.com' * sdk.utils.parseURL('hello@email.com'); // Returns 'mailto:hello@email.com' */ service.parseURL = function (string) { if (!string) { return false; } const relative = string.startsWith("/"); if (relative) { return string; } if (string.startsWith("://")) { return string; } if (string.startsWith("mailto:")) { return string; } if (string.startsWith("tel:")) { return string; } if (string.startsWith("sms:")) { return string; } ////////////////////////////////// //If someone entered an email by accident var email = service.parseEmail(string); //Convert it to a mailto link if (email && email === string) { return `mailto:${email}`; } ////////////////////////////////// const withHttp = (string) => !/^https?:\/\//i.test(string) ? `http://${string}` : string; string = withHttp(string); //TODO: test this more. string = string.replace(/\s/g, ""); // Validate the host portion in linear time. The previous single-regex // validation suffered catastrophic backtracking (40s+ freezing the main // thread) on long URLs it couldn't match, and rejected legitimate URLs with // hyphens in the query string. Anything after the host is accepted as-is. var host = String(string) .replace(/^https?:\/\//i, "") .split(/[/?#]/)[0]; var portIndex = host.lastIndexOf(":"); if (portIndex !== -1) { if (!/^\d+$/.test(host.slice(portIndex + 1))) { return false; } host = host.slice(0, portIndex); } if (!host) { return false; } var labels = host.split("."); var isIPv4 = labels.length === 4 && labels.every(function (label) { return /^\d{1,3}$/.test(label); }); if (!isIPv4) { // Domains need at least two labels ("roger" is invalid, "www.roger" is // allowed) and an alphabetic final label of 2+ characters if (labels.length < 2) { return false; } var validLabels = labels.every(function (label) { return /^[a-z\d]([a-z\d-]*[a-z\d])?$/i.test(label); }); if (!validLabels || !/^[a-z]{2,}$/i.test(labels[labels.length - 1])) { return false; } } /////////////////////////// return withHttp(string); }; /////////////////////////////////////////////// /** * A helper function for getting a number from some input * @alias utils.parseNumber * @param {String} value The input value * @param {Number} decimalPoints The number of decimal points to round to * @return {Number} the parsed number value * @example * * sdk.utils.parseNumber('123'); // Returns 123 * sdk.utils.parseNumber('75.501', 2); // Returns 75.5 * sdk.utils.parseNumber(''); // Returns 0 * sdk.utils.parseNumber(null); // Returns 0 * sdk.utils.parseNumber(); // Returns 0 */ service.parseNumber = function (input, decimalPoints) { if (!input) { return 0; } input = Number(input); if (isNaN(input)) { return 0; } if (decimalPoints) { var str = input.toFixed(decimalPoints); input = service.parseNumber(str); } return input; }; /** * A helper function for getting a number from some input * @alias utils.isValidEmailAddress * @param {String} emailAddress The email to validate * @return {Boolean} whether or not the input is a valid email address * @example * * sdk.utils.isValidEmailAddress('123.com'); // Returns false * sdk.utils.isValidEmailAddress('hello@world.com'); // Returns true * sdk.utils.isValidEmailAddress('something@special.io'); // Returns true */ service.isValidEmailAddress = function (email) { var tester = /^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; if (!email) return false; var emailParts = email.split("@"); if (emailParts.length !== 2) return false; var account = emailParts[0]; var address = emailParts[1]; if (account.length > 64) return false; else if (address.length > 255) return false; var domainParts = address.split("."); if ( domainParts.some(function (part) { return part.length > 63; }) ) return false; if (!tester.test(email)) return false; return true; }; /** * A helper function for getting a formatted email address * @alias utils.parseEmail * @param {String} emailAddress The input to parse as an email * @return {String|Boolean} a valid email address in lowercase, or false if parse is not possible * @example * * sdk.utils.parseEmail('ToMack@gmail.com'); // returns 'tomack@gmail.com' * sdk.utils.isValidEmailAddress('hello.world.com'); // Returns false */ service.parseEmail = function (input) { if (!input) { return false; } var lowercase = String(input).toLowerCase(); var valid = service.isValidEmailAddress(lowercase); if (!valid) { return false; } else { return lowercase; } }; ////////////////////////////////// /** * A helper function for getting an integer * @alias utils.parseInt * @param {String|Number} input The input to parse as an integer * @return {Integer} the resulting integer or 0 * @example * * sdk.utils.parseInt('134'); // returns 134 * sdk.utils.parseInt('cows'); // returns 0 * sdk.utils.parseInt(); // returns 0 */ service.parseInt = function (input) { if (!input) { return 0; } input = parseInt(input); if (isNaN(input)) { return 0; } return input; }; /** * A helper function for cleaning an input value to match * a required type * @alias utils.cleanValue * @param {*} data The input to clean * @param {String} type The data type to parse * @param {Object} options Additional options for parsing * @return {*} the resulting cleaned value * @example * * sdk.utils.cleanValue({_id:'1234', title:'Item'...}, 'reference'); // returns '1234' * sdk.utils.cleanValue('true', 'boolean'); // returns true * sdk.utils.cleanValue('Mr Rogers House', 'key'); // returns 'mrRogersHouse'; * sdk.utils.cleanValue('Hello.World@email.COM', 'email'); // returns 'hello.world@email.com'; */ service.cleanValue = function (data, type, options) { if (!options) { options = {}; } //////////////////////// var noInputValue = service.exists(data); if (options.strict && noInputValue && type != "boolean") { return data; } //////////////////////// switch (type) { case "reference": return service.id(data, true); break; case "boolean": return service.parseBoolean(data); break; case "url": return service.parseURL(data); break; case "key": return service.machineName(data); break; case "email": return service.parseEmail(data); break; case "date": var parsed = service.parseDate(data, options); if (parsed === undefined) { return; } else { return parsed; } break; case "number": case "decimal": case "float": var val = service.parseNumber(data); if (String(val) == String(Number(data))) { return val; } return; break; case "integer": var val = service.parseInt(data); if (String(val) == String(parseInt(data))) { return val; } return; break; case "group": return data; break; case "object": if (typeof data === "object" && data !== null && !Array.isArray(data)) { return data; } return; break; case "string": if (service.exists(data)) { var stringed = String(data); if (stringed == "[object Object]") { return; } return stringed; } else { return; } // if(!data) { // return ''; // } // if(Array.isArray(data)) { // return JSON.stringify(data); // } // if(Object.isObject(data)) { // return JSON.stringify(data); // } // return String(data); // break; default: return String(data); break; } }; service.isValidValue = function (value, dataType, strict) { var isValue = service.exists(value); var valueIsNumber = typeof value == "number"; //////////////////////// var isValidEntry = false; if (!isValue) { return false; } //////////////////////// switch (dataType) { case "url": if (strict) { isValidEntry = value.startsWith("/") || value.startsWith("://") || value.startsWith("http://") || value.startsWith("https://"); } else { isValidEntry = String(value) === service.parseURL(value); } break; case "key": isValidEntry = String(value) === service.machineName(value); break; case "date": //If we are being strict if (strict) { //And the input is not a javascript date object if (typeof value != "object" || !(value instanceof Date)) { return false; } } /////////////////////////////// var parsed = service.parseDate(value); return parsed !== undefined; break; case "email": if (strict) { isValidEntry = value === service.parseEmail(value); } else { isValidEntry = String(value).toLowerCase() === service.parseEmail(value); } break; case "number": case "decimal": case "float": if (strict) { isValidEntry = valueIsNumber && Number(value) === service.parseNumber(value); } else { isValidEntry = Number(value) === service.parseNumber(value); } break; case "integer": if (strict) { isValidEntry = valueIsNumber && Number(value) === service.parseInt(value); } else { isValidEntry = Number(value) === service.parseInt(value); } break; case "boolean": var parsed = service.parseBoolean(value); //Only accept true booleans if (strict) { if (value === true || value === false) { isValidEntry = value === parsed; } } else { isValidEntry = parsed === true || parsed === false; } break; case "reference": if (strict) { isValidEntry = String(value) === service.id(value); } else { isValidEntry = !!service.id(value); } break; case "string": var checkString = String(value); if (strict) { if (typeof value != "string") { return false; } } if (checkString == "[object Object]") { return false; } isValidEntry = true; break; // case 'group': // isValidEntry = _isObject(value); // break; case "object": isValidEntry = typeof value === "object" && value !== null && !Array.isArray(value); break; case "array": isValidEntry = Array.isArray(value); break; } //////////////////////// return isValidEntry; }; /** * A helper function for parsing input as boolean values * @alias utils.parseBoolean * @param {*} value The input to parse * @return {Boolean} the resulting true/false value * @example * * sdk.utils.parseBoolean('true'); // returns true * sdk.utils.parseBoolean('y'); // returns true * sdk.utils.parseBoolean('YES'); // returns true * sdk.utils.parseBoolean('1'); // returns true * sdk.utils.parseBoolean('t'); // returns true * sdk.utils.parseBoolean(''); // returns false * sdk.utils.parseBoolean('0'); // returns false * sdk.utils.parseBoolean('n'); // returns false * sdk.utils.parseBoolean('no'); // returns false * sdk.utils.parseBoolean('f'); // returns false * sdk.utils.parseBoolean('null'); // returns false */ service.parseBoolean = function (value) { switch (String(value).toLowerCase()) { case "true": case "y": case "yes": case "1": case "t": value = true; break; case "false": case "n": case "no": case "0": case "f": case "undefined": case "null": case "": case "-1": value = false; break; } return !!value; }; /////////////////////////////////////////////////////////////////////////////// service.clone = function (input) { return JSON.parse(JSON.stringify(input)); }; /////////////////////////////////////////////////////////////////////////////// service.getAllFields = function (actualDefinition) { const self = this; const isProfile = actualDefinition.definesType === "profile" || actualDefinition.key === "profile"; const isFormSubmission = actualDefinition.definesType === "submission"; var allFields = [...actualDefinition.fields]; var definedFields = actualDefinition.definedFields || []; if (definedFields.length) { if (isFormSubmission) { var formDataFields = { title: `Form Data`, minimum: 1, maximum: 1, key: "formData", asObject: true, type: "group", fields: definedFields, }; allFields.push(formDataFields); const cleanedDataFields = definedFields.map(function (field) { if (field.type === "reference") { field = JSON.parse(JSON.stringify(field)); delete field.fields; } return field; }); var dataFields = { title: `Data`, minimum: 1, maximum: 1, key: "data", asObject: true, type: "group", fields: cleanedDataFields, }; allFields.push(dataFields); } else { var dataFields = { title: `${actualDefinition.title}`, minimum: 1, maximum: 1, key: "data", asObject: true, type: "group", fields: definedFields, }; allFields.push(dataFields); } } if (isProfile) { allFields.push({ title: "Age", key: "_age", minimum: 1, maximum: 1, type: "integer", }); allFields.push({ title: "Date of birth", key: "_dob", minimum: 1, maximum: 1, type: "date", }); } var mapped = service .mapFields(allFields) .filter(function (field) { var isObject = field.type == "group" && field.asObject && field.minimum == 1 && field.maximum == 1; return !isObject; }) .map(function (field) { field.title = field.filterTitle || field.titles.filter(Boolean).join(" › "); return field; }) .sort(function (a, b) { return a.title < b.title ? -1 : 1; }); return mapped; }; /////////////////////////////////////////////////////////////////////////////// service.mapFields = function (fields, options) { if (!options) { options = {}; } ////////////////////////////// var output = []; var trail = []; var titles = []; var currentDepth = 1; var depthLimit = options.depth; var delimiter = options.arrayDelimeter || "[]"; //Loop through each field fields.forEach(mapField); ////////////////////////////// //Recursively map the fields function mapField(field, i) { var fieldKey = field.key; var isGroup = field.type == "group"; var singleValue = field.minimum === field.maximum && field.minimum === 1; var asObject = field.asObject; // || (isGroup && singleValue); var isLayout = isGroup && !asObject; ////////////////////////////////// // Whether to clone the field or use the existing reference var mapped = options.original ? field : service.clone(field); mapped.trail = trail.slice(); mapped.trail.push(fieldKey); mapped.path = mapped.trail.join("."); mapped.titles = titles.slice(); mapped.titles.push(field.filterTitle || field.title || ""); const isNotLayoutOrIsAllowed = !isLayout || options.includeLayout; //If its an actual element or we've asked to include //layout only elements if (isNotLayoutOrIsAllowed) { //Add it to the mix output.push(mapped); } ////////////////////////////////// //Now see if there are child fields and should we go further var limitHit = depthLimit && currentDepth >= depthLimit; //If there are child fields for this field if (field.fields && field.fields.length) { //If it's just a group with no extra key if (isLayout) { //Loop through fields as if they are at the same depth field.fields.forEach(mapField); } else { //We don't need to traverse any further //because we hit the limit if (limitHit) { return; } //Move down a level and add to the object //Include the key in the path trail currentDepth++; let injectKey = fieldKey; if (options.includeArrayDelimeter) { if (!singleValue) { injectKey = `${fieldKey}${delimiter}`; } } trail.push(injectKey); titles.push(field.filterTitle || field.title); //Loop through each field field.fields.forEach(mapField); //Move back up a level before we go to the next field currentDepth--; trail.pop(); titles.pop(); } } } return output; }; /////////////////////////////////////////////////////////////////////////////// /** * A helpful function that can take a keyed object literal and map it to url query string parameters * @alias utils.mapParameters * @param {Object} parameters The object you want to transalte * @return {String} The query string * @example * //Returns &this=that&hello=world * sdk.utils.mapParameters({"this":"that", "hello":"world"}) */ service.mapParameters = function (parameters) { parameters = parameters || {}; var array = []; Object.entries(parameters).forEach(function ([key, value]) { if (value === undefined || value === null || value == false) { return; } if (Array.isArray(value)) { value.forEach(function (v) { array.push(`${key}=${encodeURIComponent(v)}`); }); } else { array.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)); } }); return array.join("&"); }; /////////////////////////////////////////////////////////////////////////////// /** * A function that will take an integer and a currency string and return a formatted numeric amount rounded to 2 decimal places * @alias utils.formatCurrency * @param {Integer} value The amount in cents * @param {String} currency The currency to format * @return {String} The formatted value * @example * * //Returns £10.00 * sdk.utils.formatCurrency(1000, 'gbp'); * * //Returns $10.00 * sdk.utils.formatCurrency(1000, 'usd'); * */ service.formatCurrency = function (value, currency, decimalPoints) { if (!value || isNaN(value)) { value = 0; } decimalPoints = decimalPoints || 2; var currencyPrefix = service.currencySymbol(currency); return `${currencyPrefix}${parseFloat(Number(value) / 100).toFixed(decimalPoints)}`; }; /** * A function that will take an id and return the type key * @alias utils.getTypeFromID * @param {String} id The id of an object * @return {String} The key * @example * * // Returns 'user' * sdk.utils.getTypeFromID('52b523f775beea960013f6cd'); * * // Returns 'role' * sdk.utils.getTypeFromID('62b59cb572fb4772e7b5fa93'); * */ service.getTypeFromID = function (id) { id = service.id(id); if (!id) { return; } const lookup = { 21: "organisation", 23: "comment", 24: "persona", 25: "audio", 26: "campaign", 28: "smartlist", 29: "cache", 40: "application", 41: "action", 42: "billinginvoice", 43: "componentsnapshot", 44: "diff", 45: "email", 46: "submission", 49: "import", 53: "socket", 54: "timetrigger", 61: "article", 63: "component", 65: "event", 66: "file", 67: "tag", 69: "image", 70: "profile", 72: "role", 73: "scope", 74: "transaction", 75: "user", 76: "video", 77: "workflowcard", 78: "variable", "3c": "patoken", "7b": "uatoken", "7d": "urtoken", "2d": "systemtask", "3d": "export", "5f": "systemflag", "2b": "batch", "3a": "log", "2e": "stat", "2f": "unsubscribe", "6b": "intent", "7c": "upload", "2c": "systemtrigger", "6e": "interface", "4e": "interfacesnapshot", "4f": "notification", "2a": "definition", "7e": "definitionsnapshot", "5e": "integration", "4c": "listener", "6d": "topic", "7a": "code", "4d": "paymentmethod", "6c": "sslcertificate", }; var hexKey = id.slice(8, 10); return lookup[hexKey]; }; /** * A function that will take a currency string and return the symbol * @alias utils.currencySymbol * @param {String} currency The currency * @return {String} The symbol * @example * * //Returns £ * sdk.utils.currencySymbol('gbp'); * * //Returns $ * sdk.utils.currencySymbol('usd'); * */ const CURRENCY_SYMBOLS = { gbp: "\u00A3", eur: "\u20AC" }; service.currencySymbol = function (currency) { return CURRENCY_SYMBOLS[String(currency).toLowerCase()] || "$"; }; const SUPPORTED_CURRENCIES = [ { code: "usd", countries: ["US"] }, { code: "gbp", countries: ["GB", "UK"] }, { code: "cad", countries: ["CA"] }, { code: "aud", countries: ["AU"] }, { code: "nzd", countries: ["NZ"] }, { code: "sgd", countries: ["SG"] }, ]; service.getAvailableCurrencies = function (defaultCountryID) { let currencies = SUPPORTED_CURRENCIES.map(({ code, countries }) => ({ name: `${code.toUpperCase()} (${service.currencySymbol(code)})`, value: code, countryCode: Object.fromEntries(countries.map((c) => [c, true])), })); if (defaultCountryID) { const idx = currencies.findIndex((c) => c.countryCode[defaultCountryID]); if (idx > 0) { const [match] = currencies.splice(idx, 1); currencies.unshift(match); } } return currencies; }; /////////////////////////////////////////////////////////////////////////////// /** * Creates a fast keyed hash object from an array of items * @alias utils.hash * @param {Array} array The array of items to convert into a hash * @param {String} key The key or path to the property on each item to use as the hashed key * @return {Object} A key/value paired object * @example * //Returns { jimbo:{id:'jimbo', title:'Jim Jones'}, {id:'roger', title:'Roger Fellow'} } * sdk.utils.hash([{id:'jimbo', title:'Jim Jones'}, {id:'roger', title:'Roger Fellow'}], 'id'); * */ service.hash = function (items, key) { items = !Array.isArray(items) ? [] : items; return items.reduce(function (memo, item) { const k = key ? _get(item, key) : item; memo[k] = item; return memo; }, {}); }; ////////////////////////////////////////////////// /** * Returns a subset of values in an array that match a provided rule * @alias utils.extractFromArray * @param {Array} array The array you want to extract values from * @param {String} key The path to the child property you want to extract * @param {Boolean} sum Whether to sum the extracted values together in total * @param {Boolean} flatten Whether to flatten nested child arrays * @param {Boolean} unique Whether to only return unique values * @param {Boolean} exclude Whether to exclude null or undefined values * @param {Object} options Pass through extra options for how to extract the values * @return {Array} An array of all values retrieved from the array, unless provided arguments require otherwise * @example * //Returns [12, 45] as all the values * sdk.utils.extractFromArray([{name:'Wendy', age:12}, {name:'Roger', age:45}], 'age'); * * //Returns 32 * sdk.utils.extractFromArray([{name:'Wendy', age:12}, {name:'Roger', age:20}], 'age', {sum:true}); * */ service.extractFromArray = function ( array, key, sum, flatten, unique, exclude, options, ) { options = options || {}; if (sum) { options.sum = sum; } if (flatten) { options.flatten = true; } if (unique) { options.unique = true; } if (exclude) { options.excludeNull = true; } ///////////////// //Filter the array options by a certain value and operator var extractedValues = array.reduce(function (set, entry) { //Get the value from the object var retrievedValue = _get(entry, key); var isNull = !retrievedValue && retrievedValue !== false && retrievedValue !== 0; if (options.excludeNull && isNull) { return set; } set.push(retrievedValue); return set; }, []); if (options.flatten) { extractedValues = extractedValues.flat(); } if (options.unique) { extractedValues = [...new Set(extractedValues)]; } if (options.sum) { extractedValues = extractedValues.reduce(function (a, b) { return a + b; }, 0); } return extractedValues; }; ////////////////////////////////////////////////////// /** * A function that can return a selection of values that were found in an array that match a specific rule, * This is often used to evaluate expressions within form fields * @alias utils.matchInArray * @param {Array} array The array to check * @param {String} key The javascript dot notation path to extract * @param {String} value The value to compare against * @param {String} comparator The logical operator to use to compare the extracted value with the provided value ('>', '<', '>=', '<=', 'in', '==') Defaults to '==' (Is equal to) * @return {Array} Returns an array of matching values * @example * //Returns [{name:'Michael', age:45}] as that is only item in the array that matches the criteria * sdk.utils.matchInArray([{name:'Wendy', age:12}, {name:'Michael', age:45}], 'age', 45, '>='); * */ service.matchInArray = function (array, key, v, comparator) { //Filter the array options by a certain v and comparator var matches = array.filter(function (entry) { //Get the v from the object var extracted = _get(entry, key); var found; switch (comparator) { case "in": found = extracted.includes(v); break; case ">=": found = extracted >= v; break; case "<=": found = extracted <= v; break; case "<": found = extracted < v; break; case ">": found = extracted > v; break; case "==": default: if (v === undefined) { found = extracted; } else { found = extracted == v; } break; } return found; }); return matches; }; /////////////////////////////////////////////////////////////////////////////// /** * A helpful class that can take an array of values and return them as a comma seperated * string, If the values are objects, then a property to use as the string representation can be specified * @alias utils.comma * @param {Array} array The array of values to translate * @param {String} path An optional property key to use for each value * @return {String} The resulting comma seperated string * @example * //Returns 'cat, dog, bird' * sdk.utils.comma(['cat', 'dog', 'bird']); * * //Returns 'cat, dog, bird' * sdk.utils.comma([{title:'cat'}, {title:'dog'}, {title:'bird'}], 'title'); */ service.comma = function (array, path, limit) { if (limit) { array = array.slice(0, limit); } return array .filter(function (item) { return item != null && item !== undefined && item !== ""; }) .map(function (item) { if (path && path.length) { return _get(item, path); } return item; }) .join(", "); }; /////////////////////////////////////////////////////////////////////////////// //Helper function to get an id of an object /** * Returns a specified _id for an object * @alias utils.id * @param {Object} input An object that is or has an _id property * @param {Boolean} asObjectID Whether to convert to a Mongo ObjectId * @return {String} Will return either a string or a Mongo ObjectId * * @example * * //Returns '5cb3d8b3a2219970e6f86927' * sdk.utils.id('5cb3d8b3a2219970e6f86927') * * //Returns true * typeof service.id({_id:'5cb3d8b3a2219970e6f86927', title, ...}) == 'string'; * //Returns true * typeof service.id({_id:'5cb3d8b3a2219970e6f86927'}, true) == 'object'; */ service.id = function (source) { if (!source) { return; } ///////////////////////////////// var output; if (source._id) { output = String(source._id); } else { output = String(source); } var isValid = service.isValidID(output); if (!isValid) { return; } return output; }; /////////////////////////////////////////////////////////////////////////////// /** * Cleans and maps an array of objects to an array of IDs * @alias utils.ids * @param {Array} array An array of objects or object ids * @param {Boolean} asObjectID Whether or not to map the ids as Mongo ObjectIds * @return {Array} An array of Ids * * @example * //Returns ['5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86927'] * sdk.utils.ids([{_id:'5cb3d8b3a2219970e6f86927'}, {_id:'5cb3d8b3a2219970e6f86927'}, null, '5cb3d8b3a2219970e6f86927']) */ service.ids = function (array) { if (!array) { return []; } ///////////////////////////////// var ids = Object.keys( array.reduce(function (set, entry) { if (!entry) { return set; } var cleaned = service.id(entry); if (cleaned) { set[cleaned] = 1; } return set; }, {}), ); /////////////////////////////// return ids; }; /////////////////////////////////////////////////////////////////////////////// service.isValidID = function (input) { var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$"); return checkForHexRegExp.test(String(input)); }; /////////////////////////////////////////////////////////////////////////////// /** * Helper function for retrieving a human readable error message from server error response objects * @alias utils.errorMessage * @param {Object} error The error object to translate * @return {String} The resulting human readable error message */ service.errorMessage = function (err) { if (!err) { return; } if (Array.isArray(err)) { err = err[0]; } // Walk through the error object to find the most useful message let message = err?.response?.data?.message || (typeof err?.response?.data === "string" ? err.response.data : null) || err?.message || null; if (Array.isArray(message)) { message = message[0]; } return message || JSON.stringify(err); }; //////////////////////////////////// /** * Generates a globally unique ID, helpful for adding unique keys for iterable loops * @alias utils.guid * @return {String} The new globally unique identifier * @example * //Returns 4323a78br-z16h-289j-zwl1-938lda334asd * sdk.utils.guid() */ service.guid = function () { if (typeof crypto !== "undefined" && crypto.randomUUID) { return crypto.randomUUID(); } // Fallback for environments without crypto.randomUUID return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); }); }; //////////////////////////////////// //////////////////////////////////// //////////////////////////////////// /** * Helper function for cleaning strings to use as database ids * @alias utils.machineName * @param {String} string The string to clean eg. (Awesome Event!) * @return {String} A cleaned and formatted string eg. (awesomeEvent) */ service.machineName = function (string) { if (!string || !string.length) { return; } var regexp = /[^a-zA-Z0-9-_]+/g; return String(string) .replace(regexp, " ") .split("_") .map((part) => _camelCase(part)) .join("_"); }; ///////////////////////////////////////////// ///////////////////////////////////////////// ///////////////////////////////////////////// export default service; ///////////////////////////////////////////// /** * A lightweight event emitter that can be attached to any service object * to provide pub/sub event capabilities. Can be called with or without `new`. */ export function EventDispatcher() { const handlers = new Map(); const emitter = { dispatch(event, details) { const fns = handlers.get(event); if (fns) { for (const fn of fns) { fn(details); } } }, addEventListener(event, callback) { if (typeof callback !== "function") { throw new TypeError( `Expected a function for addEventListener, received ${typeof callback}`, ); } if (!handlers.has(event)) { handlers.set(event, new Set()); } handlers.get(event).add(callback); }, removeEventListener(event, callback) { const fns = handlers.get(event); if (fns) { fns.delete(callback); if (fns.size === 0) { handlers.delete(event); } } }, removeAllListeners() { handlers.clear(); }, bootstrap(target) { if (!target) return; target.dispatch = emitter.dispatch; target.addEventListener = emitter.addEventListener; target.removeEventListener = emitter.removeEventListener; target.removeAllListeners = emitter.removeAllListeners; }, }; return emitter; }