From 4d29b013a47285b4f3fbc63360995d4794be5ce8 Mon Sep 17 00:00:00 2001 From: Nic Date: Tue, 19 Dec 2017 20:17:18 +0800 Subject: [PATCH] feat: Add support to chain deployment and aliasing in a single command --- .eslintrc.json | 29 +++++ .gitignore | 3 + LICENSE | 30 +++++ README.md | 133 +++++++++++++++++++++ index.js | 24 ++++ package.json | 40 +++++++ src/nowf.js | 121 +++++++++++++++++++ src/utilities.js | 300 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 680 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 src/nowf.js create mode 100644 src/utilities.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3488db4 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "never" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fd8e52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.DS_Store +blabla.js \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08d251f --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For google-graphql-functions software + +Copyright (c) 2017, Neap pty ltd. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d95838 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +Neap Pty Ltd logo + +# NowFlow - Automate your Zeit Now Deployments +[![NPM][1]][2] + +[1]: https://img.shields.io/npm/v/now-flow.svg?style=flat +[2]: https://www.npmjs.com/package/now-flow + +Define your alias and all your environment variables inside your traditional __*now.json*__, and let __*now-flow*__ do the rest. + +_now.json_ +```js +{ + "env": { + "production": { + "scripts": { + "start": "NODE_ENV=production node index.js" + }, + "alias": "yourapp-prod" + }, + "test": { + "scripts": { + "start": "NODE_ENV=test node index.js" + }, + "alias": "yourapp-test" + } + } +} +``` + +``` +nowflow production +``` + +The above will make sure that the _package.json_ that is being deployed will contain the _start_ script `"NODE_ENV=production node index.js"` and that once the deployment to [Zeit](https://zeit.co/now) is finished, it is automatically aliased to `yourapp-prod`. + +No more deployment then aliasing steps. No more worries that some environment variables have been properly deployed to the right environment. + + +# Install +Either install it globally +``` +npm install now-flow -g +``` + +Or embed it inside your project to run it through npm + +``` +npm install now-flow --save +``` + +# How To Use It +## Basic +You must first create a __*now.json*__ file in the root of your project's directory as follow: +```js +{ + "env": { + "production": { + "alias": "yourapp-prod" + }, + "test": { + "alias": "yourapp-test" + } + } +} +``` + +Make sure there is at least one environment defined under the _env_ property. Then simply run: + +``` +nowflow production +``` + +The above will: +1. Deploy your project to [Zeit](https://zeit.co/now) using the _production_ config defined in the _now.json_. +2. Will alias that deployment using the alias defined in the _production_ config defined in the _now.json_ (i.e. 'yourapp-prod'). + +## Skipping Aliasing +If you do not want to alias your deployment, use the following: +``` +nowflow production --noalias +``` + +## Modifying The package.json's "scripts" property For Each Environment +As described in the intro, this is one of the key feature of _now-flow_. In the _now.json_, under each specific environment, you can add a __*"script"*__ property that will completely override the one defined inside the _package.json_ during the deployment. Once the deployment is completed, the _package.json_ is restored to its original state. + +```js +{ + "env": { + "production": { + "scripts": { + "start": "NODE_ENV=production node index.js" + }, + "alias": "yourapp-prod" + }, + "test": { + "scripts": { + "start": "NODE_ENV=test node index.js" + }, + "alias": "yourapp-test" + } + } +} +``` + +``` +nowflow production +``` + +In the example above, we're making sure that the _package.json_ contains a _start_ script so that _now_ can, for example, correctly start an express server. + +# This Is What We re Up To +We are Neap, an Australian Technology consultancy powering the startup ecosystem in Sydney. We simply love building Tech and also meeting new people, so don't hesitate to connect with us at [https://neap.co](https://neap.co). + +# License +Copyright (c) 2017, Neap Pty Ltd. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of Neap Pty Ltd nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL NEAP PTY LTD BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/index.js b/index.js new file mode 100644 index 0000000..a299aa6 --- /dev/null +++ b/index.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node +/** + * Copyright (c) 2017, Neap Pty Ltd. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ +'use strict' + +const program = require('commander') +const { deploy } = require('./src/nowf') + +program + .command('* ') + .usage('Deploy to Zeit Now using a specific environment configuration that will configure the \'now.json\' and the \'package.json\' accordingly.') + .option('-n, --noalias', 'When specified, prevent deployment to be aliased.') + .action((env, options={}) => { + deploy(env, ((options.parent || {}).rawArgs || []).some(x => (x == '--noalias' || x == '--noa'))) + }) + +/*eslint-disable */ +program.parse(process.argv) +/*eslint-enable */ \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce2eb15 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "now-flow", + "version": "0.0.1", + "description": "Add deployment workflows to Zeit now", + "main": "index.js", + "bin": { + "nowflow": "./index.js" + }, + "scripts": { + "eslint": "eslint ./src index.js --fix", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nicolasdao/now-workflow.git" + }, + "keywords": [ + "Zeit", + "now", + "deploy", + "deployment", + "worflow" + ], + "author": "Nicolas Dao", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/nicolasdao/now-workflow/issues" + }, + "homepage": "https://github.com/nicolasdao/now-workflow#readme", + "dependencies": { + "colors": "^1.1.2", + "commander": "^2.12.2", + "lodash": "^4.17.4", + "rimraf": "^2.6.2", + "shortid": "^2.2.8" + }, + "devDependencies": { + "eslint": "^4.13.1" + } +} diff --git a/src/nowf.js b/src/nowf.js new file mode 100644 index 0000000..5fe3308 --- /dev/null +++ b/src/nowf.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2017, Neap Pty Ltd. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ +const path = require('path') +require('colors') +const { writeToFile } = require('./utilities').files + +/*eslint-disable */ +const getAbsPath = relativePath => path.join(process.cwd(), relativePath) +const exit = msg => { + if (msg) + console.log(msg) + process.exit() +} +/*eslint-enable */ + +const duplicate = obj => obj ? JSON.parse(JSON.stringify(obj)) : obj + +const updatePackageScripts = (env='default') => { + let nowConfig + nowConfig = require(getAbsPath('now.json')) || {} + + const envConfig = (nowConfig.env || {})[env] + if (!envConfig) + throw new Error(`No environment named '${env.bold}' in ${'now.json'.bold}`) + + const pkgPath = getAbsPath('package.json') + if (envConfig.scripts != undefined) { + const currentPkg = require(pkgPath) || {} + const newPkg = duplicate(currentPkg) + + if (!currentPkg.scripts) + newPkg.scripts = {} + + for (let script in envConfig.scripts) + newPkg.scripts[script] = envConfig.scripts[script] + + return updateJsonFile(pkgPath, newPkg) + .then(() => ({ path: pkgPath, current: currentPkg, new: newPkg })) + // If updating the package.json fails, attempt to revert + .catch(err => { + return updateJsonFile(pkgPath, currentPkg) + .catch(() => null) + .then(() => { throw err }) + }) + } + else + return Promise.resolve({ path: pkgPath, current: null }) +} + +const updateNowAlias = (env='default', toogle=true) => { + const nowPath = getAbsPath('now.json') + const currentNowConfig = require(nowPath) || {} + const newNowConfig = duplicate(currentNowConfig) + + const envConfig = (currentNowConfig.env || {})[env] + if (!envConfig) + throw new Error(`No environment named '${env.bold}' in ${'now.json'.bold}`) + + if (envConfig.alias != undefined && toogle) { + newNowConfig.alias = envConfig.alias + + return updateJsonFile(nowPath, newNowConfig) + .then(() => ({ alias: toogle, path: nowPath, current: currentNowConfig, new: newNowConfig })) + // If updating the now.json fails, attempt to revert + .catch(err => { + return updateJsonFile(nowPath, currentNowConfig) + .catch(() => null) + .then(() => { throw err }) + }) + } + else + return Promise.resolve({ alias: false, path: nowPath, current: null }) +} + +const updateJsonFile = (filePath, jsonObj={}) => { + const str = JSON.stringify(jsonObj, null, '\t') + return writeToFile(filePath, str) +} + +const deploy = (env='default', noalias=false) => updatePackageScripts(env) + .catch(err => exit(err.message.italic.red)) + .then(pkg => { + try { + require('child_process').execSync('now', { stdio: 'inherit' }) + } + catch(err) { + return updateJsonFile(pkg.path, pkg.current) + .then(() => exit()) + .catch(() => exit()) + } + + return updateJsonFile(pkg.path, pkg.current) + }) + .catch(err => exit(err.message.italic.red)) + .then(() => { + if (!noalias) + return updateNowAlias(env).then(now => { + if (now.alias) { + try { + require('child_process').execSync('now alias', { stdio: 'inherit' }) + } + catch(err) { + return updateJsonFile(now.path, now.current) + .then(() => exit()) + .catch(() => exit()) + } + + return updateJsonFile(now.path, now.current) + } + }) + }) + +module.exports = { + deploy +} + diff --git a/src/utilities.js b/src/utilities.js new file mode 100644 index 0000000..dbc39a6 --- /dev/null +++ b/src/utilities.js @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2017, Neap Pty Ltd. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ +const _ = require('lodash') +const shortid = require('shortid') +const fs = require('fs') +const path = require('path') +const rimraf = require('rimraf') + +const chain = value => ({ next: fn => chain(fn(value)), val: () => value }) +const set = (obj, prop, value, mutateFn) => + !obj || !prop ? obj : + chain(typeof(prop) != 'string' && prop.length > 0).next(isPropArray => isPropArray + ? prop.reduce((acc, p, idx) => { obj[p] = value[idx]; return obj }, obj) + : (() => { obj[prop] = value; return obj })()) + .next(updatedObj => { + if (mutateFn) mutateFn(updatedObj) + return updatedObj + }) + .val() +const throwError = (v, msg) => v ? (() => {throw new Error(msg)})() : true + +const log = (msg, name, transformFn) => chain(name ? `${name}: ${typeof(msg) != 'object' ? msg : JSON.stringify(msg)}` : msg) + /*eslint-disable */ + .next(v => transformFn ? console.log(chain(transformFn(msg)).next(v => name ? `${name}: ${v}` : v).val()) : console.log(v)) + /*eslint-enable */ + .next(() => msg) + .val() + +const newShortId = () => shortid.generate().replace(/-/g, 'r').replace(/_/g, '9') + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////////////////////////// START FORMATTING //////////////////////////////// + +const zeroPad = (num, places) => { + const zero = places - num.toString().length + 1 + return Array(+(zero > 0 && zero)).join('0') + num +} +const removeMultiSpaces = s => s.replace(/ +(?= )/g,'') + +////////////////////////// END FORMATTING //////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Merge obj1 with obj2 into a new object. If there are + * conflicting properties, the one with a defined value wins. If + * they both have a value, obj1 will win. + * + * @param {object} obj1 Object 1 + * @param {object} obj2 Object 2 + * @return {object} New Object + */ +const mergeTwoObjects = (obj1, obj2) => (!obj1 || !obj2) ? (obj1 || obj2) : + ((o1, o2) => { + let newObj = {} + for(let i in o2) + newObj[i] = o1[i] != undefined ? o1[i] : o2[i] + for(let i in o1) + newObj[i] = o1[i] != undefined ? o1[i] : o2[i] + return newObj + })(obj1, obj2) + +const mergeObjects = objs => _(objs).reduce((a,b) => mergeTwoObjects(a,b), {}) + +const mapToDefault = (obj, defaultObj, mapFn) => { + const mergedObj = mergeTwoObjects(obj, defaultObj) + return (mergedObj && mapFn) + ? mergeTwoObjects(mapFn(mergedObj), mergedObj) + : mergedObj +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////////////////////////// START HTTP /////////////////////////////// + +const shortRequest = req => ({ + body: req.body, + headers: req.headers, + url: req.url, + method: req.method, + params: req.params, + query: req.query +}) + +const shortResponse = res => ({ + body: res.body, + headers: res.headers, + url: res.url, + method: res.method, + params: res.params, + query: res.query, + statusCode: res.statusCode, + statusMessage: res.statusMessage +}) + +const getCookieKeyValue = s => { + const parts = s.split('=') + return [parts[0], parts.slice(1, parts.length).join('=')] +} +const getCookie = req => (req.headers.cookie || '').split('; ').map(x => getCookieKeyValue(x)).reduce((a,b) => { a[b[0]] = b[1]; return a },{}) + +////////////////////////// END HTTP /////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////////////////////////// START CACHE /////////////////////////////// + +let _cache = {} +let _cacheClearTTL = { lastUpdate: Date.now() } +const setCache = (key, results, ttl) => { _cache[key] = { ttl: ttl, date: Date.now(), results }; return results } +const getCacheKeyValuePair = (key, cacheTTL) => { + let v = _cache[key] + const ttl = v && v.ttl ? v.ttl : cacheTTL + if (v && ((Date.now() - v.date) > ttl)) + v = undefined + clearCache(cacheTTL) + return { key, value: v ? v.results : undefined } +} + +const clearCache = cacheTTL => () => { + const now = Date.now() + if (now - _cacheClearTTL.lastUpdate > cacheTTL) { + /*eslint-disable */ + setImmediate(() => { + /*eslint-enable */ + for (let i in _cache) { + const v = _cache[i] + const ttl = v.ttl || cacheTTL + if ((now - v.date) > ttl) delete _cache[i] + } + }) + _cacheClearTTL.lastUpdate = Date.now() + } +} +const createKey = (obj, fnName) => Promise.resolve(`${fnName}:${_(obj).map((value, key) => `${key}:${value}`).join('-')}`) + +const cacheResolve = cacheTTL => (keyObj, fnName, getValue, ttl) => createKey(keyObj, fnName) + .then(key => getCacheKeyValuePair(key, cacheTTL)) + .then(cached => cached.value || setCache(cached.key, getValue(), ttl)) + +const createCacheObject = cacheTTL => ({ + /** + * Gets the cached value of the functions executed inside 'getValue'. If no values has been cached yet, then + * the 'getValue' function is executed using the parameters of the 'keyObj' and the cache is set using the concatenation + * of the 'fnName' value with the 'keyObj'. Example: + * + * const getProducts = ({ name, brandName }) => get( + * {name, brandName}, + * 'GETPRODUCTS', + * () => doSomethingLong(name, brandName), + * 10000) + * + * @param {Object} keyObj Object that represents the parameters of the functions executed inside 'getValue'. + * @param {String} fnName Function unique identifier. + * @param {Function} getValue Function being executed if the cache has not been set yet. This function does not accept any argument. + * @param {Int} ttl Time to live of the cache in milliseconds. + * @return {Promise} Promise returning the value of the cache. + */ + get: cacheResolve(cacheTTL) +}) + +////////////////////////// END CACHE /////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////////////////////////// START FILE HELPERS //////////////////////// + +const fileExists = p => new Promise((onSuccess, onFailure) => fs.exists(p, exists => exists ? onSuccess(p) : onFailure(p))) + +const createDir = p => new Promise((onSuccess, onFailure) => fs.mkdir(p, err => err ? onFailure(p) : onSuccess(p))) + +const deleteFolder = f => new Promise(onSuccess => rimraf(f, () => onSuccess())) + +const writeToFile = (filePath, stringContent) => new Promise((onSuccess, onFailure) => fs.writeFile(filePath, stringContent, err => + err ? onFailure(err) : onSuccess())) + +/** + * Creates folders under a rootFolder + * @param {String} rootFolder Root folder. This folder must exist prior to calling this function. + * @param {Array} folders Array of folders so that the path of the last item in that array will be: + * rootFolder/folders[0]/folders[1]/.../folders[n] + * @param {Object} options + * @param {Boolean} options.deletePreviousContent If set to true, this will delete the content of the existing folder + * @return {String} Path of the latest folder: + * rootFolder/folders[0]/folders[1]/.../folders[n] + */ +const createFolders = (rootFolder, folders=[], options={}) => { + const { deletePreviousContent } = options + if (!rootFolder) + throw new Error('\'rootFolder\' is required.') + return fileExists(rootFolder) + .then(() => folders.reduce((job, f) => job.then(rootPath => { + const folderPath = path.join(rootPath, f) + return fileExists(folderPath) + .then(() => deletePreviousContent + ? deleteFolder(folderPath).then(() => createDir(folderPath)).then(() => folderPath) + : folderPath) + .catch(() => createDir(folderPath).then(() => folderPath)) + }), Promise.resolve(rootFolder))) + .catch(() => { + throw new Error(`Root folder ${rootFolder} does not exist.`) + }) +} + +////////////////////////// END FILE HELPERS //////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////////////////////////// START PROMISE HELPERS //////////////////////// + + +const delayFn = (fn,time) => makeQueryablePromise((new Promise((onSuccess) => setTimeout(() => onSuccess(), time))).then(() => fn())) +const makeQueryablePromise = promise => { + // Don't modify any promise that has been already modified. + if (promise.isResolved) return promise + + // Set initial state + let isPending = true + let isRejected = false + let isFulfilled = false + + // Observe the promise, saving the fulfillment in a closure scope. + let result = promise.then( + v => { + isFulfilled = true + isPending = false + return v + }, + e => { + isRejected = true + isPending = false + throw e + } + ) + + result.isFulfilled = () => isFulfilled + result.isPending = () => isPending + result.isRejected = () => isRejected + return result +} + +////////////////////////// END PROMISE HELPERS //////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const hours = h => h * 3600000 +const minutes = m => m * 60000 +const seconds = s => s * 1000 +const millis = s => s + +const defaultCacheTTL = 5000 +module.exports = { + chain, + throwError, + log, + set, + newShortId, + formatting: { + removeMultiSpaces, + zeroPad + }, + mergeObjects, + mapToDefault, + http: { + shortRequest, + shortResponse, + getCookie + }, + cache: createCacheObject(defaultCacheTTL), + time: { + hours, + minutes, + seconds, + millis + }, + files:{ + fileExists, + createDir, + deleteFolder, + writeToFile, + createFolders + }, + promise:{ + delayFn, + makeQueryablePromise + } +} \ No newline at end of file