├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE.txt ├── README.md ├── env.js ├── fetch.js ├── lib ├── Thorin.js ├── core │ ├── boot.js │ ├── config.js │ ├── intent.js │ ├── logger.js │ └── thorinCore.js ├── interface │ ├── IModule.js │ ├── ISanitizer.js │ ├── IStore.js │ └── ITransport.js ├── routing │ ├── action.js │ ├── authorization.js │ ├── dispatcher.js │ ├── middleware.js │ └── validator.js └── util │ ├── attached.js │ ├── errors.js │ ├── event.js │ ├── fetch.js │ └── util.js ├── npm-shrinkwrap.json ├── package.json ├── spawn.js └── update.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .thorin 3 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | boilerplate 3 | .idea -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 UNLOQ Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thorin.js core component 2 | 3 | ## Full documentation available at https://thorinjs.com -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'), 3 | fs = require('fs'), 4 | fse = require('fs-extra'); 5 | /** 6 | * Manually set data to the .thorin config file. 7 | * */ 8 | const THORIN_ROOT = process.cwd(); 9 | let PERSIST_FILE = path.normalize(THORIN_ROOT + '/config/.thorin'); 10 | if(typeof process.pkg !== 'undefined') { 11 | PERSIST_FILE = path.normalize(THORIN_ROOT + '/.thorin'); 12 | } 13 | 14 | function getConfig() { 15 | try { 16 | fse.ensureFileSync(PERSIST_FILE); 17 | } catch (e) { 18 | console.warn(`thorin-env: could not ensure file exists: ${PERSIST_FILE}`); 19 | return false; 20 | } 21 | let config = ''; 22 | try { 23 | config = fs.readFileSync(PERSIST_FILE, {encoding: 'utf8'}); 24 | } catch (e) { 25 | console.warn(`thorin-env: could not read file: ${PERSIST_FILE}`); 26 | return false; 27 | } 28 | if (typeof config === 'string' && config) { 29 | try { 30 | config = JSON.parse(config); 31 | if (typeof config !== 'object' || !config) config = {}; 32 | } catch (e) { 33 | config = {}; 34 | } 35 | } 36 | if (typeof config !== 'object' || !config) config = {}; 37 | return config; 38 | } 39 | 40 | /* Manually set the config data in the file. NOTE THIS DOES NOT MERGE, JUST REPLACE. */ 41 | function setConfig(config) { 42 | let oldConfig = getConfig(); // to check if the file exists. 43 | if (!oldConfig) return false; 44 | if (typeof config !== 'object' || !config) return false; 45 | let configData = ''; 46 | try { 47 | configData = JSON.stringify(config, null, 1); 48 | } catch (e) { 49 | console.warn(`thorin-env: failed to serialize configuration`, e); 50 | return false; 51 | } 52 | try { 53 | fs.writeFileSync(PERSIST_FILE, configData, {encoding: 'utf8'}); 54 | } catch (e) { 55 | console.warn(`thorin-env: failed to persist new config in .thorin file`); 56 | console.debug(e); 57 | return false; 58 | } 59 | return true; 60 | } 61 | 62 | module.exports.env = function SetEnv(key, value) { 63 | let config = getConfig(); 64 | if (typeof config._APP_ENV !== 'object' || !config._APP_ENV) { 65 | config._APP_ENV = {}; 66 | } 67 | if (typeof key === 'object' && key) { 68 | Object.keys(key).forEach((name) => { 69 | config._APP_ENV[name] = key[name]; 70 | }); 71 | } else if (typeof key === 'string' && typeof value !== 'undefined') { 72 | config._APP_ENV[key] = value; 73 | } 74 | return setConfig(config); 75 | }; 76 | -------------------------------------------------------------------------------- /fetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const thorinFetch = require('./lib/util/fetch'); 3 | const extend = require('extend'), 4 | ThorinError = require('./lib/util/errors'); 5 | /* 6 | * This is the raw thorin fetcher 7 | * */ 8 | const wrapper = { 9 | util: { 10 | extend: function (source) { 11 | let target = {}; 12 | let args = Array.prototype.slice.call(arguments); 13 | args.reverse(); 14 | args.push(target); 15 | args.push(true); 16 | args = args.reverse(); 17 | return extend.apply(extend, args); 18 | } 19 | }, 20 | error: (code, message, status) => { 21 | let e = new ThorinError.generic(code, message); 22 | if (typeof status === 'number') { 23 | e.statusCode = status; 24 | } 25 | e.ns = 'FETCH'; 26 | return e; 27 | } 28 | }; 29 | module.exports = thorinFetch(wrapper).fetch; 30 | -------------------------------------------------------------------------------- /lib/Thorin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const finallyPolyfill = require('promise.prototype.finally'); // .finally() polyfill 3 | finallyPolyfill.shim(); 4 | const fs = require('fs'), 5 | path = require('path'), 6 | cluster = require('cluster'), 7 | async = require('async'), 8 | utils = require('./util/util'), 9 | ThorinCore = require('./core/thorinCore'), 10 | ThorinConfig = require('./core/config'), 11 | ThorinBoot = require('./core/boot'), 12 | dispatcherInit = require('./routing/dispatcher'), 13 | intentInit = require('./core/intent'), 14 | initLogger = require('./core/logger'), 15 | initAction = require('./routing/action'), 16 | initMiddleware = require('./routing/middleware'), 17 | initAuthorization = require('./routing/authorization'); 18 | 19 | const info = { 20 | docker: false, // This will be set to true if we are inside a docker container. The check is done in /proc/1/cgroup (only works on linux) 21 | modulePaths: [], 22 | version: null, // the app's version, defaults to the one found under package.json 23 | root: typeof global.THORIN_ROOT === 'string' ? global.THORIN_ROOT : process.cwd(), // the current working dir. 24 | id: null, 25 | app: null, // the current app name, defaults to the js file name. 26 | env: null, // the current application environment. 27 | argv: {}, // hash of startup options, with no -- or - 28 | pkg: {}, // the application's package.json file. 29 | persistFile: 'config/.thorin', // the default location where we can persist framework specific stuff. 30 | configSources: [], // array of configuration sources to look for configs. 31 | rootConfig: {} // the main configuration of the Thorin app. 32 | }; 33 | /* If we're packaged as a binary with pkg, we change it to .thorin */ 34 | if (typeof process.pkg !== 'undefined') { 35 | info.persistFile = '.thorin'; 36 | } 37 | 38 | /* Verify if we have any .env file in the root project. If we do, we set them in process.env */ 39 | (function () { 40 | function setValues(env) { 41 | env = env.replace(/\r\n/g, "\r").replace(/\n/g, "\r").split(/\r/); 42 | for (let i = 0; i < env.length; i++) { 43 | let val = env[i].trim(); 44 | if (!val) continue; 45 | let eq = val.indexOf('='); 46 | if (eq === -1) continue; 47 | let key = val.substr(0, eq), 48 | value = val.substr(eq + 1); 49 | process.env[key] = value; 50 | } 51 | } 52 | 53 | // Try and set from root/.env 54 | try { 55 | let env = fs.readFileSync(info.root + '/.env', {encoding: 'utf8'}); 56 | setValues(env); 57 | } catch (e) { 58 | // Try and set from root/config/.env 59 | try { 60 | let env = fs.readFileSync(info.root + '/config/.env', {encoding: 'utf8'}); 61 | setValues(env); 62 | } catch (e) { 63 | } 64 | } 65 | })(); 66 | 67 | class ThorinApp extends ThorinCore { 68 | 69 | constructor() { 70 | super(); 71 | this.dispatcher = dispatcherInit(this); // give it a thorin reference. 72 | this.Intent = intentInit(this); // this is a thorin intent, used by transports. 73 | this.Action = initAction(this); // this is a thorin Action (or route) defining a chain of middleware for it. 74 | this.Middleware = initMiddleware(this); // this is the middleware class. 75 | this.Authorization = initAuthorization(this); // this is the authorization class. 76 | this.logger = initLogger(this); // logger repository 77 | this.initialized = false; 78 | this.running = false; 79 | this.globalize('thorin'); 80 | } 81 | 82 | /* 83 | * Loads all the node.js components (or the actual file) for the given path. 84 | * This is done recursively for directories. 85 | * NOTE: 86 | * when calling loadPath, all arguments except the first one are proxied to the require(), if the require 87 | * exposes a function. 88 | * */ 89 | loadPath(fullPath, ignoreInitialized) { 90 | if (typeof fullPath === 'string') { 91 | fullPath = [fullPath]; 92 | } 93 | let args = Array.prototype.slice.call(arguments); 94 | args.splice(0, 1); // remove the first path 95 | if (!(fullPath instanceof Array)) { 96 | console.warn('Thorin.loadPath: works with loadPath(path:string)'); 97 | return this; 98 | } 99 | fullPath.forEach((p) => { 100 | if (typeof p !== 'string') { 101 | console.warn('Thorin.loadPath: path ' + p + ' is not a string.'); 102 | return; 103 | } 104 | if (this.initialized || ignoreInitialized === true) { 105 | if (ignoreInitialized === true) { 106 | args.splice(1, 1); // remove ignoreInitialized 107 | } 108 | bootObj.loadPath({ 109 | path: p, 110 | args: args 111 | }); 112 | } else { 113 | info.modulePaths.push({ 114 | path: p, 115 | args: args 116 | }); 117 | } 118 | }); 119 | return this; 120 | } 121 | 122 | /* Returns the project's root path */ 123 | get root() { 124 | return info.root; 125 | } 126 | 127 | set root(v) { 128 | info.root = v; 129 | } 130 | 131 | /* Returns your application's package.json content */ 132 | get package() { 133 | return info.pkg; 134 | } 135 | 136 | /* Returns the application's version found under package.json, or set's the given one. */ 137 | get version() { 138 | if (!info.version) info.version = info.pkg.version; 139 | return info.version; 140 | } 141 | 142 | set version(v) { 143 | if (typeof v === 'string' || typeof v === 'number') { 144 | info.version = v; 145 | } 146 | return this; 147 | } 148 | 149 | /* 150 | * Returns the application's environment. 151 | * Looks into: 152 | * --env= or --environment= in argv 153 | * NODE_ENV in env variables 154 | * development, as default 155 | * */ 156 | get env() { 157 | if (info.env) return info.env; 158 | let e = this.argv('env', null) || this.argv('environment', null) || process.env.NODE_ENV; 159 | if (e) { 160 | info.env = e; 161 | } else { 162 | info.env = 'development'; 163 | } 164 | process.env.NODE_ENV = info.env; 165 | return info.env; 166 | } 167 | 168 | /* 169 | * Returns the unique app ID. 170 | * */ 171 | get id() { 172 | if (!info.id) { 173 | let appData = bootObj.readConfig(); 174 | if (!appData) appData = {}; 175 | if (appData.id) { 176 | info.id = appData.id; 177 | } else { 178 | let appId = utils.randomString(12); 179 | info.id = this.app + "-" + appId; 180 | bootObj.writeConfig({ 181 | id: info.id 182 | }); 183 | } 184 | // IF we're clustered, we will add the worker id in the thorin id. 185 | if (cluster.isWorker) { 186 | info.id += '-' + cluster.worker.id; 187 | } 188 | } 189 | return info.id; 190 | } 191 | 192 | /* 193 | * Sets or gets the thorin app's name, defaulting to the js file name. 194 | * */ 195 | get app() { 196 | if (!info.app) { 197 | info.app = global.THORIN_APP || path.basename(process.mainModule.filename).replace('.js', ''); 198 | bootObj.writeConfig({ 199 | app: info.app 200 | }); 201 | } 202 | return info.app; 203 | } 204 | 205 | set app(_name) { 206 | if (typeof _name === 'string' && _name) { 207 | info.app = _name; 208 | bootObj.writeConfig({ 209 | app: info.app 210 | }); 211 | } 212 | } 213 | 214 | get docker() { 215 | return info.docker; 216 | } 217 | 218 | set docker(v) { 219 | } 220 | 221 | /* Searches the proces's argv array for the given key. If not found, 222 | * returns the default avlue provided or null 223 | * */ 224 | argv(keyName, _default) { 225 | if (typeof keyName !== 'string' || !keyName) return _default || null; 226 | keyName = keyName.replace(/-/g, ''); 227 | if (info.argv[keyName.toLowerCase()]) return info.argv[keyName] || _default || null; 228 | return null; 229 | } 230 | 231 | /* 232 | * Cleans up the process.env variables to avoid leaking any kind of env data. 233 | * */ 234 | cleanEnv() { 235 | Object.keys(process.env).forEach((key) => { 236 | process.env[key] = '_'; 237 | }); 238 | } 239 | 240 | /* 241 | * Globalizes the thorin framework. By default, we do so under global['thorin'] 242 | * */ 243 | globalize(_val) { 244 | if (_val === false && typeof global['thorin'] !== 'undefined') { 245 | delete global['thorin']; 246 | delete global['async']; 247 | } else if (typeof _val === 'string') { 248 | global[_val] = this; 249 | global['async'] = async; 250 | } 251 | return this; 252 | } 253 | 254 | /* Persists the given thorin data to the .thorin file. 255 | * This will be mainly used by plugins that require some kind of state 256 | * between restarts. 257 | * */ 258 | persist(configKey, configData) { 259 | if (typeof configKey !== 'string') return this; 260 | /* IF we do not have configData, we return the previous data. */ 261 | if (typeof configData === 'undefined') { 262 | let oldConfig = bootObj.readConfig(); 263 | if (typeof oldConfig !== 'object' || !oldConfig) return null; 264 | return oldConfig[configKey] || null; 265 | } 266 | let toWrite = {}; 267 | toWrite[configKey] = configData; 268 | bootObj.writeConfig(toWrite); 269 | return this; 270 | } 271 | 272 | /* 273 | * Adds a new entry to the .gitignore file, if it does not exist. 274 | * */ 275 | addIgnore(entry) { 276 | if (typeof process.pkg !== 'undefined') return true; 277 | let gitIgnore = path.normalize(this.root + '/.gitignore'), 278 | ignoreContent = ''; 279 | try { 280 | ignoreContent = fs.readFileSync(gitIgnore, {encoding: 'utf8'}); 281 | } catch (e) { 282 | } 283 | if (ignoreContent.indexOf(entry) === -1) { 284 | ignoreContent += '\n' + entry + '\n'; 285 | try { 286 | fs.writeFileSync(gitIgnore, ignoreContent, {encoding: 'utf8'}); 287 | } catch (e) { 288 | console.warn('Thorin: failed to update .gitignore file:', e); 289 | } 290 | } 291 | return true; 292 | } 293 | 294 | /* 295 | * This will register a new configuration source. 296 | * Configuration sources load config from different places. 297 | * The default one is from the local app/config "disk". 298 | * */ 299 | addConfig(name, _opt, done) { 300 | if (typeof name !== 'string' && typeof name !== 'function') { 301 | throw new Error('Thorin.addConfig: name must be either a string or a function.'); 302 | } 303 | if (!_opt) _opt = {}; 304 | let item = { 305 | options: _opt 306 | }; 307 | if (typeof name === 'string') { 308 | item.name = name; 309 | item.type = "thorin"; // this is a thorin config, we look into ThorinConfig 310 | } else { 311 | item.type = "fn"; 312 | item.fn = name; 313 | } 314 | if (!this.initialized) { 315 | info.configSources.push(item); 316 | } else { 317 | bootObj.loadAppConfig(item, (e, newConfig) => { 318 | if (e) return done && done(e); 319 | if (this.config) { 320 | this.config.merge(newConfig); 321 | return done && done(); 322 | } 323 | process.nextTick(() => { 324 | this.config.merge(newConfig); 325 | done && done(); 326 | }); 327 | }); 328 | } 329 | return this; 330 | } 331 | 332 | /* 333 | * Runs the Thorin app. 334 | * */ 335 | run(onDone) { 336 | if (this.initialized) return onDone && onDone(); 337 | let calls = [], 338 | self = this; 339 | 340 | /* Check if we have a --config={configFilePath} that will override all previously loaded configs */ 341 | let cmdConfigPath = this.argv('config'); 342 | if (cmdConfigPath) { 343 | this.addConfig('disk', { 344 | path: cmdConfigPath, 345 | absolute: true 346 | }); 347 | } 348 | 349 | /* Next, read all the configuration from all its sources. */ 350 | calls.push((done) => { 351 | bootObj.loadAppConfig(info.configSources, (e, rootConfig) => { 352 | if (e) return done(e); 353 | this.config = ThorinConfig.Instance(rootConfig); 354 | this.initialized = true; 355 | this._triggerThorinEvent(ThorinCore.EVENT.CONFIG, "thorin.core"); 356 | done(); 357 | }); 358 | }); 359 | 360 | /* Mount all the components. */ 361 | calls.push((done) => { 362 | this.createComponents(done); 363 | }); 364 | 365 | 366 | /* Next, mount the components with access to their configuration. */ 367 | calls.push((done) => { 368 | this.initComponents(done); 369 | }); 370 | 371 | /* Next, load all the app's middleware and actions. */ 372 | calls.push((done) => { 373 | info.modulePaths.forEach((p) => { 374 | bootObj.loadPath(p); 375 | }); 376 | done(); 377 | }); 378 | 379 | /* Next, if we have any kind of --setup= argument, we will call the setup() function of the components */ 380 | calls.push((done) => { 381 | this.setupComponents(done); 382 | }); 383 | 384 | /* Finally, run all the components. */ 385 | calls.push((done) => { 386 | this.runComponents(done); 387 | }); 388 | 389 | 390 | async.series(calls, (err) => { 391 | if (err) { 392 | console.error('Thorin: failed to initialize application:'); 393 | console.trace(err); 394 | return onDone && onDone(err); 395 | } 396 | // finally, tell the dispatcher to run. 397 | this.dispatcher.start(); 398 | this.running = true; 399 | this._triggerThorinEvent(ThorinCore.EVENT.RUN, "thorin.core"); 400 | this._removeThorinEvents(ThorinCore.EVENT.RUN); 401 | onDone && onDone(); 402 | }); 403 | } 404 | 405 | } 406 | 407 | const appObj = new ThorinApp(); 408 | process.on('uncaughtException', (e) => appObj.exit(e)); 409 | /* perpetuate the statics */ 410 | Object.keys(ThorinCore).forEach((key) => { 411 | appObj[key] = ThorinCore[key]; 412 | }); 413 | const bootObj = new ThorinBoot(appObj); 414 | bootObj.init(info); 415 | bootObj.bootstrap(); 416 | 417 | module.exports = appObj; 418 | -------------------------------------------------------------------------------- /lib/core/boot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'), 3 | os = require('os'), 4 | fse = require('fs-extra'), 5 | async = require('async'), 6 | attached = require('../util/attached'), 7 | utils = require('../util/util.js'), 8 | ThorinConfig = require('./config'), 9 | path = require('path'); 10 | /** 11 | * Boot loader file, parses arguments and processes data. 12 | */ 13 | let persistFile, 14 | isInDocker = false, 15 | loadedPaths = Symbol(); 16 | 17 | /* Check if we're inside docker. */ 18 | try { 19 | if (os.platform().indexOf('win') === -1) { 20 | let procFile = fs.readFileSync(`/proc/1/cgroup`, {encoding: 'utf8'}); 21 | if (procFile.indexOf('docker') !== -1) { 22 | isInDocker = true; 23 | } else if(procFile.indexOf('kubepod') !== -1) { 24 | isInDocker = true; 25 | } 26 | } 27 | } catch (e) { 28 | } 29 | global.THORIN_DOCKER = isInDocker; 30 | global.THORIN_CONFIG = (typeof global.THORIN_CONFIG === 'string' ? global.THORIN_CONFIG : path.basename(process.argv[1])); 31 | module.exports = class ThorinBoot { 32 | 33 | constructor(app) { 34 | this[loadedPaths] = {}; // a hash of loaded modules, so we do not require twice. 35 | this.app = app; 36 | this.info = null; 37 | this.configName = global.THORIN_CONFIG; // the key in the .thorin file will always be the node.js file name 38 | } 39 | 40 | /* 41 | * Bootstraps all libraries that use thorin's utilities. 42 | * */ 43 | bootstrap() { 44 | this.app.util = utils; // thorin.util; 45 | /* load up all interfaces in thorin.Interface */ 46 | let ifaces = utils.readDirectory(__dirname + '/../interface', { 47 | extension: 'js' 48 | }); 49 | 50 | /* globalize the logger. */ 51 | this.app.logger.globalize('log'); // default log.debug in global. 52 | 53 | /* Attach the Thorin interfaces. */ 54 | for (let i = 0; i < ifaces.length; i++) { 55 | let iClass = require(ifaces[i]), 56 | iName = iClass.name; 57 | if (iName.charAt(0) === 'I') iName = iName.substr(1); 58 | this.app.Interface[iName] = iClass; 59 | } 60 | /* Attach additional utilities */ 61 | attached(this.app); 62 | 63 | /* Attach the default functionality */ 64 | this.app.addSanitizer('thorin-sanitize'); 65 | 66 | /* Add the default app paths. */ 67 | /* NOTE: Apps that run in "test" mode, weill not load app/ 68 | * NOTE2: to disable autoloading, set global.THORIN_AUTOLOAD = false; 69 | * NOTE3: if the file is under tests/ we set autoload to false. 70 | * */ 71 | let jsFilePath = process.argv[1].split(path.sep), 72 | tmp1 = jsFilePath.pop(), 73 | tmp2 = jsFilePath.pop(); 74 | if (tmp1 === 'tests' || tmp2 === 'tests') { 75 | global.THORIN_AUTOLOAD = false; 76 | } 77 | if (this.app.app !== 'test' && global.THORIN_AUTOLOAD !== false) { 78 | if (this.app.app !== 'build') { // build apps will not have their actions/ loaded. 79 | this.app.loadPath(['app/actions', 'app/middleware']); 80 | } 81 | const isSetup = this.app.argv('setup', null); 82 | if (isSetup && typeof process.pkg === 'undefined') { 83 | /* Ensure the app file system structure */ 84 | try { 85 | fse.ensureDirSync(this.app.root + '/config/env'); 86 | } catch (e) { 87 | } 88 | try { 89 | fse.ensureDirSync(this.app.root + '/app'); 90 | } catch (e) { 91 | } 92 | } 93 | } 94 | } 95 | 96 | /* Initializes the arguments, package.json and other such. */ 97 | init(info) { 98 | this.info = info; 99 | info.docker = isInDocker; 100 | /* set the info argv. */ 101 | let items = []; 102 | for (let i = 2; i < process.argv.length; i++) { 103 | let tmp = process.argv[i].split(' '); 104 | items = items.concat(tmp); 105 | } 106 | for (let i = 0; i < items.length; i++) { 107 | let tmp = items[i].split('='); 108 | if (tmp.length === 0) continue; 109 | let k = tmp[0], 110 | v = tmp[1] || ''; 111 | k = k.replace(/-/g, ''); 112 | if (v === 'true' || v === 'false') { 113 | v = (v === 'true'); 114 | } else if (v.indexOf(',') !== -1) { 115 | v = v.split(','); 116 | } 117 | info.argv[k] = v; 118 | } 119 | /* Read the current package.json to have it in-memory for later user. */ 120 | var pkg; 121 | try { 122 | pkg = fs.readFileSync(path.normalize(info.root + "/package.json"), {encoding: 'utf8'}); 123 | info.pkg = JSON.parse(pkg); 124 | } catch (e) { 125 | console.error('Thorin: failed to read the project package.json:', e); 126 | } 127 | 128 | /* Checks if we have a .gitignore file in the root dir. If not, we add one. */ 129 | this.app.addIgnore('.thorin'); 130 | 131 | /* Load our thorin config and check if we have to set any ENV variables */ 132 | let config = this.readConfig(true); 133 | if (typeof config === 'object' && config) { 134 | if (typeof config._APP_ENV === 'object' && config._APP_ENV) { 135 | Object.keys(config._APP_ENV).forEach((name) => { 136 | process.env[name] = config._APP_ENV[name]; 137 | }); 138 | } 139 | } 140 | } 141 | 142 | set persistFile(v) { 143 | persistFile = v; 144 | } 145 | 146 | get persistFile() { 147 | if (persistFile) return persistFile; 148 | let root = (typeof process.pkg === 'undefined' ? this.info.root : process.cwd()); 149 | persistFile = path.normalize(root + '/' + this.info.persistFile); 150 | return persistFile; 151 | } 152 | 153 | 154 | /* Reads previously persisted configuration */ 155 | readConfig(_allConfig) { 156 | let oldConfig = null; 157 | try { 158 | fse.ensureFileSync(this.persistFile); 159 | } catch (e) { 160 | console.warn('Thorin: failed to read .thorin config file:', e); 161 | } 162 | try { 163 | oldConfig = fs.readFileSync(this.persistFile, {encoding: 'utf8'}); 164 | } catch (e) { 165 | console.warn('Thorin: failed to read old .thorin config: ', e); 166 | oldConfig = null; 167 | } 168 | if (typeof oldConfig === 'string') { 169 | oldConfig = oldConfig.trim(); 170 | if (oldConfig.length > 0 && oldConfig.charAt(0) !== '{' && oldConfig.charAt(0) !== '[') { 171 | console.log(this.persistFile + ' file was corrupted.'); 172 | return {}; 173 | } 174 | try { 175 | oldConfig = JSON.parse(oldConfig); 176 | } catch (e) { 177 | } 178 | } 179 | if (!oldConfig) oldConfig = {}; 180 | if (_allConfig === true) { 181 | return oldConfig; 182 | } 183 | return oldConfig[this.configName]; 184 | } 185 | 186 | /* 187 | * Persists thorin-related configurations to the .thorin file. 188 | * */ 189 | writeConfig(_data) { 190 | try { 191 | fse.ensureFileSync(this.persistFile); 192 | } catch (e) { 193 | console.error('Thorin: failed to ensure .thorin config file in %s: ', this.persistFile, e); 194 | return this; 195 | } 196 | if (typeof _data !== 'object' || !_data) return this; // nothing to persist. 197 | let oldConfig = this.readConfig(true), 198 | newConfig = {}; 199 | newConfig[this.configName] = _data; 200 | let finalConfig = utils.extend(oldConfig, newConfig), 201 | oldString = JSON.stringify(oldConfig, null, 1), 202 | newString = JSON.stringify(finalConfig, null, 1); 203 | if (oldString === newString) return this; 204 | try { 205 | fs.writeFileSync(this.persistFile, newString, {encoding: 'utf8'}); 206 | } catch (e) { 207 | console.warn('Thorin: failed to persist new config in .thorin file:', e); 208 | } 209 | } 210 | 211 | /* 212 | * Require() the given file paths. 213 | * */ 214 | loadPath(item) { 215 | if (typeof item.path !== 'string') return; 216 | let self = this, 217 | p = item.path, 218 | args = item.args; 219 | 220 | function doRequire(filePath) { 221 | if (self[loadedPaths][filePath]) return; 222 | if (!path.isAbsolute(filePath)) { 223 | filePath = path.normalize(self.app.root + '/' + filePath); 224 | } 225 | try { 226 | let stat = fs.statSync(filePath); 227 | if (!stat.isFile()) { 228 | let e = new Error(""); 229 | e.code = "NOT_FILE"; 230 | throw e; 231 | } 232 | } catch (e) { 233 | if (e.code === 'ENOENT') { 234 | console.warn(`Thorin.loadPath: module path ${filePath} not found.`); 235 | return; 236 | } 237 | if (e.code === 'NOT_FILE') { 238 | console.warn(`Thorin.loadPath: module path ${filePath} is not a file.`); 239 | return; 240 | } 241 | } 242 | 243 | let itemFn = require(filePath); 244 | if (typeof itemFn === 'function' && itemFn.autoload !== false) { 245 | // call the function passing the arguments. 246 | itemFn.apply(global, args); 247 | } 248 | self[loadedPaths][filePath] = true; 249 | } 250 | 251 | if (path.extname(p) === '.js') { 252 | doRequire(p); 253 | } else { 254 | let files = utils.readDirectory(p, { 255 | ext: '.js' 256 | }); 257 | files.forEach((p) => doRequire(p)); 258 | } 259 | } 260 | 261 | /* PUBLIC INTEREST FUNCTIONALITIES */ 262 | 263 | /* 264 | * Loads up all the thorin app's configurations, the order that they were specified. 265 | * We will always have a "disk" configuration loader, so by default we add it. 266 | * */ 267 | loadAppConfig(items, done) { 268 | // first call 269 | if (items instanceof Array) { 270 | /* Add the env=specific one. */ 271 | items.splice(0, 0, { 272 | name: "disk", 273 | type: "thorin", 274 | options: { 275 | path: 'config/env/' + this.app.env + '.js', 276 | required: false 277 | } 278 | }); 279 | 280 | /* IF the thorin apps' name is not app, add the app's name config. */ 281 | let appConfig = path.normalize(this.app.root + '/config/' + this.app.app + '.js'), 282 | defaultAppConfig = path.normalize(this.app.root + '/config/app.js'); 283 | if (utils.isFile(appConfig)) { 284 | items.splice(0, 0, { 285 | name: "disk", 286 | type: "thorin", 287 | options: { 288 | path: "config/" + this.app.app + ".js", 289 | required: false 290 | } 291 | }); 292 | } else if (utils.isFile(defaultAppConfig)) { 293 | /* Add the default app.js config if available. */ 294 | items.splice(0, 0, { 295 | name: "disk", 296 | type: "thorin", 297 | options: { 298 | path: "config/app.js", 299 | required: false 300 | } 301 | }); 302 | } 303 | /* SECRETS always come first, before anything */ 304 | let hasDockerSecrets = false; 305 | for (let i = 0, len = items.length; i < len; i++) { 306 | let itm = items[i]; 307 | if (itm.name !== 'secret') continue; 308 | let secret = items.splice(i, 1)[0]; 309 | items.unshift(secret); 310 | hasDockerSecrets = true; 311 | break; 312 | } 313 | /* IF we are in docker, we always tro to load any docker secrets and place them in the env file. */ 314 | if (global.THORIN_DOCKER && !hasDockerSecrets) { 315 | items.unshift({ 316 | name: 'secret', 317 | type: 'thorin' 318 | }); 319 | } 320 | } else { 321 | items = [items]; 322 | } 323 | let rootConfig = {}; 324 | let calls = []; 325 | /* We load up all config sources and fetch em' */ 326 | items.forEach((iObj, idx) => { 327 | calls.push((done) => { 328 | iObj.tracked = true; 329 | if (iObj.type === 'thorin') { 330 | // we have a Thorin config function. 331 | if (typeof ThorinConfig[iObj.name] === 'undefined') { 332 | return done(new Error('Thorin.loadAppConfig: configSource name ' + iObj.name + ' does not exist.')); 333 | } 334 | return ThorinConfig[iObj.name](this.app, rootConfig, iObj.options, done); 335 | } 336 | // we have a normal FN function. 337 | if (iObj.type === 'fn') { 338 | return iObj.fn(this.app, rootConfig, iObj.options, done); 339 | } 340 | return done(); // not available. 341 | }); 342 | }); 343 | 344 | /* 345 | * Next, we process the configuration. 346 | * */ 347 | calls.push((done) => { 348 | return ThorinConfig.__processConfiguration(this.app, rootConfig, done); 349 | }); 350 | async.series(calls, (err) => { 351 | if (err) { 352 | console.error('Thorin.config: failed to load all configuration sources.'); 353 | return done(err); 354 | } 355 | // see if any config file added another config 356 | let newConfigs = []; 357 | for (let i = 0; i < items.length; i++) { 358 | if (items[i].tracked) continue; 359 | newConfigs.push(items[i]); 360 | } 361 | if (newConfigs.length === 0) { 362 | return done(null, rootConfig); 363 | } 364 | this.loadAppConfig(newConfigs, done); 365 | }); 366 | } 367 | 368 | 369 | }; 370 | -------------------------------------------------------------------------------- /lib/core/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'), 4 | dotObject = require('dot-object'), 5 | extend = require('extend'), 6 | fetch = require('node-fetch'), 7 | path = require('path'), 8 | url = require('url'), 9 | util = require('../util/util'), 10 | async = require('async'); 11 | 12 | dotObject.override = true; 13 | 14 | let SCONFIG_URL = `https://${process.env.SCONFIG_GATEWAY || 'api.sconfig.io'}/config`; 15 | 16 | if (typeof process.env.SCONFIG_URL === 'string') { 17 | try { 18 | let tmp = url.parse(process.env.SCONFIG_URL); 19 | tmp.pathname = '/config'; 20 | SCONFIG_URL = url.format(tmp, false); 21 | } catch (e) { 22 | console.warn(`SConfig URL is not valid: ${process.env.SCONFIG_URL}`); 23 | } 24 | } 25 | let SCONFIG_KEY = null; 26 | 27 | /** 28 | * Created by Adrian on 19-Mar-16. 29 | * This contains information regarding how we actually load the configuration 30 | * for our process. 31 | * We can aggregate configuration from multiple sources. 32 | */ 33 | class ThorinConfig { 34 | 35 | /* 36 | * Loads up all secrets found inside the given folder. 37 | * Since we are preparing for docker secrets, we are looking at: 38 | * {fileName = the KEY} 39 | * {fileContent} = the VALUE 40 | * OPTIONS: 41 | * - path = the default is /run/secrets 42 | * NOTES: 43 | * - this will place the key/values in process.env 44 | * */ 45 | static secret(app, config, opt, onLoad) { 46 | if (!opt) opt = {}; 47 | if (!opt.path) opt.path = '/run/secrets'; 48 | opt.path = path.normalize(opt.path); 49 | let envObj = {}; 50 | try { 51 | let files = fs.readdirSync(opt.path); 52 | if (files.length === 0) return onLoad(); 53 | files.forEach((fpath) => { 54 | let filePath = path.normalize(opt.path + '/' + fpath), 55 | fileContent; 56 | try { 57 | let stat = fs.lstatSync(filePath); 58 | if (!stat.isFile()) return; 59 | } catch (e) { // not a file. 60 | return; 61 | } 62 | try { 63 | fileContent = fs.readFileSync(filePath, {encoding: 'utf8'}); 64 | if (!fileContent) return; 65 | } catch (e) { 66 | return; 67 | } 68 | envObj[fpath] = fileContent; 69 | }); 70 | Object.keys(envObj).forEach((keyName) => { 71 | process.env[keyName] = envObj[keyName]; 72 | }); 73 | onLoad(); 74 | } catch (e) { 75 | return onLoad(); 76 | } 77 | } 78 | 79 | /* 80 | * Loads up all the configuration from the "app/config" folder. 81 | * */ 82 | static disk(app, config, opt, onLoad) { 83 | if (typeof opt === 'string') { 84 | opt = { 85 | path: [opt] 86 | }; 87 | } 88 | opt = app.util.extend({ 89 | path: [], 90 | absolute: false, 91 | required: true 92 | }, opt); 93 | if (typeof opt.path === 'string') opt.path = [opt.path]; 94 | let calls = []; 95 | opt.path.forEach((configPath) => { 96 | if (!opt.absolute) { 97 | configPath = path.normalize(app.root + '/' + configPath); 98 | } 99 | /* IF the file is a .json one, we parse it. */ 100 | if (path.extname(configPath) === ".json") { 101 | calls.push((done) => { 102 | fs.readFile(configPath, {encoding: 'utf8'}, (err, diskConfig) => { 103 | if (err) { 104 | if (opt.required) { 105 | console.error('Thorin.config.disk: failed to load config from file: %s', configPath); 106 | return done(err); 107 | } 108 | return done(); 109 | } 110 | try { 111 | diskConfig = JSON.parse(diskConfig); 112 | } catch (e) { 113 | console.error('Thorin.config.disk: failed to parse config from file: %s', configPath); 114 | return done(e); 115 | } 116 | this.__processConfiguration(app, diskConfig, () => { 117 | extend(true, config, diskConfig); 118 | done(); 119 | }); 120 | }); 121 | }); 122 | } 123 | /* IF the file is a .js one, we require it and merge export. */ 124 | if (path.extname(configPath) === ".js") { 125 | calls.push((done) => { 126 | let diskConfig; 127 | try { 128 | diskConfig = require(configPath); 129 | } catch (e) { 130 | if (e.code === 'MODULE_NOT_FOUND' && !opt.required) return done(); 131 | if (opt.required) { 132 | console.error('Thorin.config.disk: failed to require config from file: %s', configPath); 133 | return done(e); 134 | } 135 | console.trace(e); 136 | } 137 | if (typeof diskConfig === 'object' && diskConfig) { 138 | return this.__processConfiguration(app, diskConfig, () => { 139 | extend(true, config, diskConfig); 140 | done(); 141 | }); 142 | } 143 | done(); 144 | }); 145 | } 146 | }); 147 | /* Otherwise, unsupported config. */ 148 | async.series(calls, onLoad); 149 | } 150 | 151 | /* 152 | * Calls SConfig with the given token, and places all the $ENV and $ARGV sources 153 | * in the final config. 154 | * OPTIONS: 155 | * - version: the version to fetch (defaults to latest) 156 | * 157 | * - key (32char) -> the 32 char key 158 | * - secret (32char) => the 32 char secret of the app 159 | * OR 160 | * - key (100+char) -> the full access token. 161 | * */ 162 | static sconfig(app, config, opt, done) { 163 | if (typeof opt !== 'object' || !opt) { 164 | console.error('Thorin.sconfig: missing key from options.'); 165 | return done(); 166 | } 167 | let key = (typeof opt.key === 'string' ? opt.key : process.env.SCONFIG_KEY), 168 | secret = (typeof opt.secret === 'string' ? opt.secret : process.env.SCONFIG_SECRET), 169 | version = (typeof opt.version === 'string' ? opt.version : process.env.SCONFIG_VERSION); 170 | if (typeof key !== 'string' || !key) { 171 | console.error('Thorin.sconfig: missing key.'); 172 | return done(); 173 | } 174 | // fetch the key from process env. 175 | if (key.indexOf('$ENV:') === 0) { 176 | let tmp = key.substr(5); 177 | key = process.env[tmp]; 178 | if (!key) { 179 | console.warn(`Thorin.config: environment variable "${tmp}" does not exist.`); 180 | return done(); 181 | } 182 | } else if (key.indexOf('$ARGV:') === 0) { 183 | let tmp = key.substr(6); 184 | key = app.argv(tmp); 185 | if (!key) { 186 | console.warn(`Thorin.config: argv variable ${tmp} does not exist.`); 187 | return done(); 188 | } 189 | } 190 | let url = SCONFIG_URL, 191 | self = this, 192 | configType = 'env'; // by default, environment key=value pairs 193 | if (typeof version !== 'undefined') { 194 | url += '?v=' + version; 195 | } 196 | 197 | function parseThorinConfig(type, tconfig, shouldExtend) { 198 | let res = {}; 199 | switch (type) { 200 | case 'json': 201 | res = tconfig; 202 | if (shouldExtend) { 203 | self.__processConfiguration(app, res, () => { 204 | extend(true, config, res); 205 | }); 206 | } 207 | break; 208 | case 'env': 209 | const items = tconfig.split('\n'); 210 | for (let i = 0; i < items.length; i++) { 211 | let item = items[i].trim(); 212 | if (item == '' || item.indexOf('=') === -1) continue; 213 | if (item.charAt(0) === '#' || (item.charAt(0) === '/' && item.charAt(1) === '/')) continue; // comments 214 | let tmp = item.split('='), 215 | key = tmp[0], 216 | val = tmp[1] || null; 217 | res[key] = val; 218 | } 219 | if (shouldExtend) { 220 | Object.keys(res).forEach((key) => { 221 | process.env[key] = res[key]; 222 | }); 223 | } 224 | break; 225 | } 226 | return res; 227 | } 228 | 229 | SCONFIG_KEY = key; 230 | let status; 231 | fetch(url, { 232 | headers: { 233 | Authorization: 'Bearer ' + key 234 | } 235 | }).then((res) => { 236 | status = res.status; 237 | if (res.status !== 200) { 238 | return res.json(); 239 | } 240 | let contentType = res.headers.get('content-type'); 241 | if (contentType.indexOf('json') !== -1) { 242 | configType = 'json'; 243 | } 244 | return res.text(); 245 | }).then((config) => { 246 | if (status !== 200) { 247 | let err = (typeof config === 'object' ? config : app.error('SCONFIG', 'Could not finalize configuration request', res.status)); 248 | throw err; 249 | } 250 | // check if we have a secret 251 | if (secret) { 252 | let decrypted = app.util.decrypt(config, secret); 253 | if (!decrypted) { 254 | console.error(`Thorin.sconfig: could not decrypt configuration data with secret`); 255 | throw 1; 256 | } 257 | config = decrypted; 258 | } 259 | if (configType === 'json') { 260 | try { 261 | config = JSON.parse(config); 262 | } catch (e) { 263 | console.warn(`Thorin.sconfig: could not parse configuration as JSON: ${config}`); 264 | throw 1; 265 | } 266 | } 267 | app.persist('sconfig_data', { 268 | type: configType, 269 | data: config 270 | }); 271 | parseThorinConfig(configType, config, true); 272 | done(); 273 | }).catch((err) => { 274 | const persistedData = app.persist('sconfig_data'); 275 | if (persistedData) { 276 | parseThorinConfig(persistedData.type, persistedData.data, true); 277 | } else { 278 | console.warn(`Thorin.sconfig: could not fallback to previously persisted configuration.`); 279 | } 280 | if (err === 1) { 281 | return done(); 282 | } 283 | console.error(`Thorin.sconfig: could not fetch configuration data.`); 284 | console.trace(err); 285 | done(); 286 | }); 287 | } 288 | 289 | /* 290 | * Adds configuration from the specified environment variable name 291 | * */ 292 | static env(app, config, opt, done) { 293 | let envName = opt.name; 294 | if (!envName) return done(); 295 | /* Load config from process.env.THORIN_CONFIG */ 296 | if (!process.env[envName]) { 297 | return done(); 298 | } 299 | let d; 300 | try { 301 | d = JSON.parse(process.env[envName]); 302 | } catch (e) { 303 | console.warn(`Thorin: could not parse env config from: ${envName}`); 304 | return done(); 305 | } 306 | this.__processConfiguration(app, d, () => { 307 | extend(true, config, d); 308 | done(); 309 | }); 310 | } 311 | 312 | /* 313 | * Processes all the $ENV and $ARG variables and looks for them, or 314 | * it will call out a warning if not present. 315 | * */ 316 | static __processConfiguration(app, config, done) { 317 | let parsedConfig = {}; 318 | Object.keys(config).forEach((keyName) => { 319 | if (keyName.indexOf('.') === -1) { 320 | let subConfig = config[keyName]; 321 | if ((typeof config[keyName] === 'object' && config[keyName] && !(config[keyName] instanceof Array))) { 322 | subConfig = dotObject.dot(subConfig); 323 | subConfig = dotObject.object(subConfig); 324 | parsedConfig[keyName] = extend(true, parsedConfig[keyName] || {}, subConfig); 325 | } else { 326 | parsedConfig[keyName] = subConfig; 327 | } 328 | } else { 329 | let tmp = {}; 330 | dotObject.str(keyName, config[keyName], tmp); 331 | parsedConfig = extend(true, parsedConfig, tmp); 332 | } 333 | delete config[keyName]; 334 | }); 335 | Object.keys(parsedConfig).forEach((keyName) => { 336 | config[keyName] = parsedConfig[keyName]; 337 | }); 338 | 339 | /* Step two, search for $ENV and $ARG */ 340 | function doReplace(str) { 341 | if (typeof str !== 'string' || !str) return str; 342 | str = str.trim(); 343 | let tmp = str.toUpperCase(); 344 | if (tmp.indexOf('$ENV:') !== -1) { 345 | let keyName = str.substr(5); 346 | if (typeof process.env[keyName] === 'undefined') { 347 | console.warn('Thorin.config: environment variable: %s not found', keyName); 348 | return ''; 349 | } 350 | return process.env[keyName]; 351 | } 352 | 353 | if (tmp.indexOf("$ARG:") !== -1) { 354 | let argName = str.substr(5), 355 | argVal = app.argv(argName, null); 356 | if (!argVal) { 357 | console.warn('Thorin.config: argument variable: %s not found', argName); 358 | return ''; 359 | } 360 | return argVal; 361 | } 362 | return str; 363 | } 364 | 365 | function checkEnvArg(data) { 366 | Object.keys(data).forEach((keyName) => { 367 | if (typeof data[keyName] === 'string') { 368 | data[keyName] = doReplace(data[keyName]); 369 | return; 370 | } 371 | if (data[keyName] instanceof Array) { 372 | for (let i = 0; i < data[keyName].length; i++) { 373 | data[keyName][i] = doReplace(data[keyName][i]); 374 | } 375 | return; 376 | } 377 | if (typeof data[keyName] === 'object' && data[keyName]) { 378 | return checkEnvArg(data[keyName]); 379 | } 380 | }); 381 | } 382 | 383 | checkEnvArg(config); 384 | done(); 385 | } 386 | 387 | /* The ThorinConfigInstance is used in thorin.config("configKey"), mounted on the actual thorin app. */ 388 | static Instance(rootConfig) { 389 | function config(keyName, _defaultValue) { 390 | if (typeof keyName === 'undefined') { 391 | return rootConfig; 392 | } 393 | 394 | _defaultValue = (typeof _defaultValue === 'undefined' ? null : _defaultValue); 395 | if (typeof keyName !== 'string' || !keyName) return _defaultValue; 396 | let val = dotObject.pick(keyName, rootConfig); 397 | if (typeof val === 'undefined') return _defaultValue; 398 | return val; 399 | } 400 | 401 | /* 402 | * Override a configuration setting. 403 | * */ 404 | config.set = function SetConfig(keyName, keyValue) { 405 | if (typeof keyName !== 'string' || !keyName || typeof keyValue === 'undefined') return this; 406 | if (keyName.indexOf('.') === -1) { 407 | rootConfig[keyName] = keyValue; 408 | } else { 409 | let newSetting; 410 | if (typeof keyValue === 'object' && keyValue) { 411 | newSetting = dotObject.object(keyValue); 412 | } else { 413 | newSetting = keyValue; 414 | } 415 | try { 416 | dotObject.remove(keyName, rootConfig); 417 | } catch (e) { 418 | } 419 | dotObject.str(keyName, newSetting, rootConfig); 420 | } 421 | return this; 422 | }; 423 | /* 424 | * Merges the given configuration with the current one. 425 | * */ 426 | config.merge = function MergeConfig(targetConfig) { 427 | if (typeof targetConfig !== 'object' || !targetConfig) return this; 428 | rootConfig = util.extend(rootConfig, targetConfig); 429 | return this; 430 | }; 431 | 432 | /* Clears the given configuration keys. */ 433 | config.clear = function ClearConfigurationKeys(_arg) { 434 | let keys = (_arg instanceof Array ? _arg : Array.prototype.slice.call(arguments)); 435 | for (let i = 0; i < keys.length; i++) { 436 | if (typeof keys[i] === 'string' && keys[i]) { 437 | dotObject.remove(keys[i], rootConfig); 438 | } 439 | } 440 | return this; 441 | }; 442 | 443 | /* Returns the sconfig Authorization key. */ 444 | config.getSconfigKey = () => SCONFIG_KEY; 445 | return config; 446 | } 447 | } 448 | 449 | module.exports = ThorinConfig; 450 | -------------------------------------------------------------------------------- /lib/core/intent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Created by Adrian on 03-Apr-16. 4 | * The Thorin Intent class is used to defined incoming events or actions. 5 | * Transports will then match intents based on their actions and 6 | * validate them. 7 | */ 8 | module.exports = function init(thorin) { 9 | const input = Symbol(), 10 | filter = Symbol(), 11 | rawInput = Symbol(), 12 | rawMeta = Symbol(), 13 | rawFilter = Symbol(), 14 | data = Symbol(), 15 | client = Symbol(), 16 | error = Symbol(), 17 | alias = Symbol(), 18 | authorization = Symbol(), 19 | authorizationSource = Symbol(), 20 | onSend = Symbol(), 21 | metaData = Symbol(), 22 | meta = Symbol(), 23 | rawResult = Symbol(), 24 | timeStart = Symbol(), 25 | timeEnd = Symbol(), 26 | events = Symbol(), 27 | eventsFired = Symbol(), 28 | resultHeaders = Symbol(), 29 | result = Symbol(); 30 | 31 | const INTENT_EVENTS = { 32 | END: 'end', 33 | CLOSE: 'close' // fired when the underlying socket is closed. 34 | }; 35 | 36 | class ThorinIntent { 37 | 38 | constructor(actionType, rawInputData, onIntentSend) { 39 | this.completed = false; 40 | // this.transport is set at the transport layer, the source transport 41 | this.proxied = false; // if the default onIntentSend callback was changed. 42 | this.action = actionType; 43 | this[rawInput] = rawInputData; 44 | //this[rawFilter] = {}; 45 | // this[rawMeta] = {}; 46 | this[rawResult] = false; // raw results will not send an object, with {result:}, but the actual value of .result() 47 | this[events] = {}; // this is our mini event handler. with only .on('event', oneFunctionCall) 48 | this[eventsFired] = {}; // a hash of {eventName:true} 49 | this[timeStart] = Date.now(); 50 | this[timeEnd] = null; 51 | this[data] = {}; // we can attach data to it. 52 | this[input] = {}; // input data 53 | //this[filter] = {}; // filter data 54 | // this[meta] = {} // meta data 55 | this[result] = null; // output data 56 | this[metaData] = null; // pagination data 57 | this[error] = null; // error info 58 | this[authorization] = null; // authorization data 59 | //this[authorizationSource] = null; // the authorization source. Where did it come from / what is it? 60 | this[client] = {}; // incoming client data. 61 | this[onSend] = onIntentSend; 62 | this[resultHeaders] = null; // a hash of headers to send back 63 | } 64 | 65 | /* Sets the authorization information for this intent. 66 | * The authorization information is set at the transport level, depending 67 | * on the source of it. 68 | * As an example, HTTP transports may place the Authorization: "Bearer {token}" header value 69 | * as the authorization data. 70 | * */ 71 | _setAuthorization(authSource, authId) { 72 | if (typeof authSource !== 'string' || !authSource) { 73 | console.error('Thorin.Intent: _setAuthorization(source, id) must contain a valid authorization source'); 74 | return this; 75 | } 76 | if (typeof authId === 'undefined') { 77 | console.error('Thorin.Intent: _setAuthorization(source, id) must have an authorization id.'); 78 | return this; 79 | } 80 | this[authorization] = authId; 81 | this[authorizationSource] = authSource; 82 | return this; 83 | } 84 | 85 | get authorization() { 86 | return this[authorization]; 87 | } 88 | 89 | set authorization(v) { 90 | console.error('Thorin.Intent: please use _setAuthorization(source, id) in stead of direct set.'); 91 | } 92 | 93 | get alias() { 94 | return this[alias] || null; 95 | } 96 | 97 | set alias(v) { 98 | if (this[alias]) return; 99 | this[alias] = v; 100 | } 101 | 102 | get authorizationSource() { 103 | return this[authorizationSource] || null; 104 | } 105 | 106 | get rawInput() { 107 | return this[rawInput]; 108 | } 109 | 110 | get rawMeta() { 111 | if (!this[rawMeta]) this[rawMeta] = {}; 112 | return this[rawMeta]; 113 | } 114 | 115 | hasFilter() { 116 | if (typeof this[rawFilter] !== 'object' || !this[rawFilter]) return false; 117 | return true; 118 | } 119 | 120 | get rawFilter() { 121 | if (!this[rawFilter]) this[rawFilter] = {}; 122 | return this[rawFilter]; 123 | } 124 | 125 | set rawInput(v) { 126 | return this; 127 | } 128 | 129 | set rawFilter(v) { 130 | return this; 131 | } 132 | 133 | set rawMeta(v) { 134 | return this; 135 | } 136 | 137 | __setRawFilter(v) { 138 | if (typeof v !== 'object' || !v) return this; 139 | this[rawFilter] = v; 140 | return this; 141 | } 142 | 143 | __setRawInput(v) { 144 | if (typeof v !== 'object' || !v) return this; 145 | this[rawInput] = v; 146 | return this; 147 | } 148 | 149 | __setRawMeta(v) { 150 | if (typeof v !== 'object' || !v) return this; 151 | this[rawMeta] = v; 152 | return this; 153 | } 154 | 155 | /* 156 | * An intent's send() function can be proxied, going through another callback 157 | * before actually calling the send() function. You can look at this like a 158 | * call that will be called before the send() function. This proxy function can alter 159 | * errors or results of the intent. 160 | * WARNING: When proxying an intent, you MUST call the send() of that intent, otherwise 161 | * it will not send the response to the client. 162 | * For example: 163 | * intentObj.proxy(function(obj) { 164 | * console.log("PROXY WITH", obj); 165 | * this.send({something: "else"}); 166 | * }); 167 | * 168 | * ///// 169 | * intentObj.send({some: "object"}) 170 | * */ 171 | proxy(proxyFn) { 172 | if (this.proxied) { 173 | console.warn('Thorin.Intent: intent for action ' + this.action + ' is already proxied.'); 174 | return this; 175 | } 176 | this.proxied = true; 177 | let oldSend = this[onSend]; 178 | this[onSend] = function ProxySend() { 179 | this[onSend] = oldSend; 180 | try { 181 | proxyFn.apply(this, arguments); 182 | } catch (e) { 183 | console.error('Thorin.intent: proxy function on intent action ' + this.action + ' threw an error.'); 184 | this.error(thorin.error(e)); 185 | this.send(); 186 | } 187 | }.bind(this); 188 | this.proxied = true; 189 | return this; 190 | } 191 | 192 | /* 193 | * When an intent is triggered by the dispatcher, this function gets called asynchronously, 194 | * in order to asynchronously initiate the intent. 195 | * A plugin or anybody can override this function to insert itself 196 | * into it, BEFORE it is actually run through its stack. 197 | * NOTE: 198 | * when overriding it, the function MUST call the callback function with (err|null) 199 | * */ 200 | runCreate(fn) { 201 | fn(); 202 | } 203 | 204 | /* 205 | * This will set some specific headers to be sent to the transport layer. 206 | * */ 207 | resultHeaders(name, _val) { 208 | if (typeof name === 'object' && name) { 209 | if (typeof this[resultHeaders] !== 'object' || this[resultHeaders] == null) this[resultHeaders] = {}; 210 | Object.keys(name).forEach((key) => { 211 | this[resultHeaders][key] = name[key]; 212 | }); 213 | return this; 214 | } 215 | if (typeof name === 'undefined') return this[resultHeaders]; 216 | if (typeof name === 'string' && typeof _val === 'undefined') { 217 | return this[resultHeaders][name] || null; 218 | } 219 | if (typeof name === 'string' && typeof _val !== 'undefined') { 220 | if (typeof this[resultHeaders] !== 'object' || !this[resultHeaders]) this[resultHeaders] = {}; 221 | this[resultHeaders][name] = _val; 222 | return this; 223 | } 224 | return null; 225 | } 226 | 227 | /* Event handler ON: will call the function only once. */ 228 | on(eventName, fn) { 229 | if (typeof fn !== 'function') { 230 | console.error('Thorin.intent: .on(eventName, fn), fn is not a function.'); 231 | return this; 232 | } 233 | if (this[eventsFired][eventName]) { 234 | try { 235 | fn(); 236 | } catch (e) { 237 | console.error('Thorin.Intent: triggered event ' + eventName + ' on callback function threw an exception.'); 238 | console.error(e); 239 | } 240 | return this; 241 | } 242 | if (typeof this[events][eventName] === 'undefined') this[events][eventName] = []; 243 | this[events][eventName].push(fn); 244 | return this; 245 | } 246 | 247 | /* 248 | * Sets/gets specific client information. 249 | * */ 250 | client(key, val) { 251 | if (typeof key === 'object' && key) { 252 | Object.keys(key).forEach((a) => { 253 | this.client(a, key[a]); 254 | }); 255 | return this; 256 | } 257 | if (typeof key === 'undefined') return this[client]; 258 | if (typeof key === 'string' && typeof val === 'undefined') { 259 | if (!this[client]) return null; 260 | return (typeof this[client][key] === 'undefined' ? null : this[client][key]); 261 | } 262 | this[client][key] = val; 263 | return this; 264 | } 265 | 266 | /* 267 | * Get/Set additional data to it. 268 | * */ 269 | data(name, _val) { 270 | if (typeof name !== 'string') return this[data]; 271 | if (typeof name === 'string' && typeof _val === 'undefined') return (typeof this[data][name] === 'undefined' ? null : this[data][name]); 272 | this[data][name] = _val; 273 | return this; 274 | } 275 | 276 | /* 277 | * Getter/setter for input data. 278 | * */ 279 | input(name, _val) { 280 | if (typeof name === 'undefined') return this[input]; 281 | if (typeof name === 'string' && typeof _val === 'undefined') { 282 | if (this[input] == null) return null; 283 | return (typeof this[input][name] === 'undefined' ? null : this[input][name]); 284 | } 285 | if (typeof name === 'string' && typeof _val !== 'undefined') { 286 | this[input][name] = _val; 287 | try { 288 | this[rawInput][name] = _val; 289 | } catch (e) { 290 | } 291 | return this; 292 | } 293 | return null; 294 | } 295 | 296 | /* 297 | * Manually override the input 298 | * */ 299 | _setInput(data) { 300 | if (typeof data === 'object' && data) { 301 | this[input] = data; 302 | } 303 | return this; 304 | } 305 | 306 | /* 307 | * Getter/setter for filter data. 308 | * */ 309 | filter(name, _val) { 310 | if (typeof name === 'undefined') return this[filter] || {}; 311 | if (typeof name === 'string' && typeof _val === 'undefined') { 312 | if (!this[filter]) return null; 313 | if (typeof this[filter] === 'undefined') return null; 314 | return (typeof this[filter][name] === 'undefined' ? null : this[filter][name]); 315 | } 316 | if (typeof name === 'string' && typeof _val !== 'undefined') { 317 | if (!this[filter]) this[filter] = {}; 318 | this[filter][name] = _val; 319 | try { 320 | this.rawFilter[name] = _val; 321 | } catch (e) { 322 | } 323 | return this; 324 | } 325 | if (typeof name === 'object' && name && typeof _val === 'undefined') { 326 | this[filter] = name; 327 | let _keys = Object.keys(name); 328 | for (let i = 0, len = _keys.length; i < len; i++) { 329 | this.rawFilter[_keys[i]] = name[_keys[i]]; 330 | } 331 | } 332 | return null; 333 | } 334 | 335 | /* 336 | * Manually set the filter 337 | * */ 338 | _setFilter(obj) { 339 | if (typeof obj === 'object' && obj) { 340 | this[filter] = obj; 341 | } 342 | return this; 343 | } 344 | 345 | /* 346 | * Getter/setter for meta data. 347 | * */ 348 | meta(name, _val) { 349 | if (typeof name === 'undefined') return this[meta] || {}; 350 | if (typeof name === 'string' && typeof _val === 'undefined') { 351 | if (!this[meta]) return null; 352 | if (typeof this[meta] === 'undefined') return null; 353 | return (typeof this[meta][name] === 'undefined' ? null : this[meta][name]); 354 | } 355 | if (typeof name === 'string' && typeof _val !== 'undefined') { 356 | if (!this[meta]) this[meta] = {}; 357 | this[meta][name] = _val; 358 | try { 359 | this.rawMeta[name] = _val; 360 | } catch (e) { 361 | } 362 | return this; 363 | } 364 | return null; 365 | } 366 | 367 | /* 368 | * Manually override the meta 369 | * */ 370 | _setMeta(data) { 371 | if (typeof data === 'object' && data) { 372 | this[meta] = data; 373 | } 374 | return this; 375 | } 376 | 377 | 378 | /* Checks if the intent is an error yet. */ 379 | hasError() { 380 | return (this[error] != null); 381 | } 382 | 383 | /* Checks if we have any result */ 384 | hasResult() { 385 | return (this[result] != null); 386 | } 387 | 388 | hasRawResult() { 389 | return (this[rawResult] === true); 390 | } 391 | 392 | /* 393 | * Sets the raw result of the intent. Raw results will send the result as is, 394 | * without wrapping it into an object. 395 | * */ 396 | rawResult(val) { 397 | if (typeof val === 'undefined') return; 398 | this[rawResult] = true; 399 | this[result] = val; 400 | return this; 401 | } 402 | 403 | /* 404 | * Manually set the given value as the intent's result. This should not be used by anyone outside the core plugins 405 | * */ 406 | _setResult(val) { 407 | if(typeof val === 'undefined') val = null; 408 | this[result] = val; 409 | return this; 410 | } 411 | 412 | /* 413 | * Get/set result information 414 | * */ 415 | result(name, val) { 416 | if (typeof name === 'string' && typeof val === 'undefined') { 417 | if (this[result] == null) return null; 418 | return this[result][name] || null; 419 | } 420 | if (name instanceof Array) { 421 | this[result] = name; 422 | return this; 423 | } 424 | if (typeof name === 'object' && name) { 425 | if (typeof name.getDataValue === 'function') { // ignore objects that have toJSON, as the transport will take care of it. 426 | this[result] = name; 427 | return this; 428 | } 429 | if (typeof name.toJSON === 'function') { 430 | this[result] = name.toJSON(); 431 | } else { 432 | this[result] = {}; 433 | Object.keys(name).forEach((key) => { 434 | this.result(key, name[key]); 435 | }); 436 | } 437 | return this; 438 | } 439 | if (typeof name === 'string' && !(this[result] instanceof Array)) { 440 | if (typeof val === 'undefined') { 441 | return (typeof this[result][name] === 'undefined' ? null : this[result][name]); 442 | } 443 | if (this[result] == null) this[result] = {}; 444 | this[result][name] = val; 445 | return this; 446 | } 447 | if (typeof name === 'undefined') { 448 | return this[result]; 449 | } 450 | return this; 451 | } 452 | 453 | /* 454 | * Verify if the current request is made by a mobile browser or not. If the intent does not have any client headers, 455 | * returns false. 456 | * */ 457 | isMobile() { 458 | let headers = this.client('headers'); 459 | if (!headers) return false; 460 | let ua = headers['user-agent']; 461 | if (typeof ua !== 'string' || !ua) return false; 462 | if (/mobile/i.test(ua)) return true; //general mobile check 463 | if (/Android/.test(ua)) return true; // android devices 464 | if (/iPhone/.test(ua)) return true; 465 | return false; 466 | } 467 | 468 | /* 469 | * Sets pagination data or other information that will be used in the root object.. The pagination data will be included in the base response, 470 | * right next to id and result. 471 | * TODO: this should work with a thorin.Pagination 472 | * */ 473 | setMeta(data, val) { 474 | if (typeof data === 'object') { 475 | if (data == null) { 476 | this[metaData] = null; 477 | return this; 478 | } 479 | Object.keys(data).forEach((k) => { 480 | this.setMeta(k, data[k]); 481 | }); 482 | return this; 483 | } 484 | if (typeof data === 'string') { 485 | if (typeof val === 'undefined') { 486 | return (typeof this[metaData][data] === 'undefined' ? null : this[metaData][data]); 487 | } 488 | if (this[metaData] == null) this[metaData] = {}; 489 | this[metaData][data] = val; 490 | return this; 491 | } 492 | return this; 493 | }; 494 | 495 | /** 496 | * Returns any metadata associated with the intent 497 | * */ 498 | getMeta() { 499 | return this[metaData] || {}; 500 | } 501 | 502 | 503 | /* 504 | * Marks the intent as error. We can set the error only once. 505 | * */ 506 | error(err) { 507 | if (typeof err === 'undefined') return this[error]; 508 | if (err instanceof Error && err.name.indexOf('Thorin') !== 0) { 509 | err = thorin.error(err); 510 | } 511 | this[error] = err; 512 | return this; 513 | } 514 | 515 | /* 516 | * Returns the number of ms it took for the intent to end. 517 | * */ 518 | get took() { 519 | if (!this[timeEnd]) return 0; 520 | return this[timeEnd] - this[timeStart]; 521 | } 522 | 523 | /* 524 | * Ends the current intent, responding with the result back to the transport layer. 525 | * */ 526 | send(obj) { 527 | if (this.proxied) { 528 | this.proxied = false; 529 | this[onSend].apply(this, arguments); // proxy the send. 530 | return; 531 | } 532 | if (this.completed) { 533 | return this; 534 | } 535 | if (!this[rawResult] && typeof obj === 'object' && obj != null) { 536 | if (obj instanceof Error && !this.hasError()) { 537 | this.error(obj); 538 | } else if (!this.hasResult()) { 539 | this.result(obj); 540 | } 541 | } 542 | this[timeEnd] = Date.now(); 543 | this.completed = true; 544 | const intentResult = { 545 | "type": this.action 546 | }; 547 | if (this.hasError()) { 548 | intentResult.error = this.error(); 549 | triggerEvent.call(this, INTENT_EVENTS.END); 550 | if (typeof this[onSend] === 'function') { 551 | this[onSend](true, intentResult, this); 552 | this.destroy(); 553 | } else { 554 | this.destroy(); 555 | return intentResult; 556 | } 557 | } else { 558 | // IF we have a raw result, we just send it. 559 | if (this[rawResult] === true) { 560 | triggerEvent.call(this, INTENT_EVENTS.END); 561 | if (typeof this[onSend] === 'function') { 562 | this[onSend](false, this[result], this); 563 | this.destroy(); 564 | } else { 565 | this.destroy(); 566 | return this[result]; 567 | } 568 | return; 569 | } 570 | 571 | // set any pagination data. 572 | if (this[metaData] != null) { 573 | intentResult.meta = {}; 574 | Object.keys(this[metaData]).forEach((k) => { 575 | if (k === 'result' || k === "action") return; 576 | if (typeof intentResult[k] !== 'undefined') return; 577 | intentResult.meta[k] = this[metaData][k]; 578 | }); 579 | } 580 | if (this.hasResult()) { 581 | intentResult.result = this.result(); 582 | } 583 | triggerEvent.call(this, INTENT_EVENTS.END); 584 | if (typeof this[onSend] === 'function') { 585 | this[onSend](false, intentResult, this); 586 | this.destroy(); 587 | } else { 588 | this.destroy(); 589 | return intentResult; 590 | } 591 | } 592 | } 593 | 594 | /* 595 | * Intent destructor 596 | * */ 597 | destroy() { 598 | this[data] = null; 599 | this[rawInput] = null; 600 | if (this[rawFilter]) this[rawFilter] = null; 601 | if (this[filter]) this[filter] = null; 602 | if (this[rawMeta]) this[rawMeta] = null; 603 | this[input] = null; 604 | this[result] = null; 605 | this[error] = null; 606 | this[authorization] = null; 607 | this[client] = null; 608 | this[metaData] = null; 609 | this[events] = {}; 610 | this[eventsFired] = {}; 611 | delete this[resultHeaders]; 612 | delete this[onSend]; 613 | } 614 | 615 | /* 616 | * Triggers an internal event. This should only be used by 617 | * other plugins or core components. 618 | * */ 619 | __trigger() { 620 | return triggerEvent.apply(this, arguments); 621 | } 622 | } 623 | ThorinIntent.EVENT = INTENT_EVENTS; 624 | 625 | /* 626 | * This is called by the intent to trigger an event 627 | * Event triggering is only for the intent to fire. 628 | * */ 629 | function triggerEvent(eventName, _args) { 630 | if (typeof this[events][eventName] === 'undefined') return; 631 | for (let i = 0; i < this[events][eventName].length; i++) { 632 | try { 633 | this[events][eventName][i](_args); 634 | } catch (e) { 635 | console.error('Thorin.Intent: triggered event ' + eventName + ' on callback function threw an exception.'); 636 | console.error(e); 637 | } 638 | } 639 | this[eventsFired][eventName] = true; 640 | delete this[events][eventName]; 641 | } 642 | 643 | return ThorinIntent; 644 | }; 645 | -------------------------------------------------------------------------------- /lib/core/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const util = require('util'), 3 | colors = require('colors'); 4 | /** 5 | * Created by Adrian on 06-Apr-16. 6 | * This is the default Thorin.js logger. It defaults 7 | * to logging stuff to the console. 8 | */ 9 | let DATE_FORMAT = "yyyy-MM-dd hh:mm:ss.SSS", 10 | LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], 11 | LOG_COLORS = { 12 | trace: colors.blue, 13 | debug: colors.cyan, 14 | info: colors.green, 15 | warn: colors.yellow, 16 | error: colors.red, 17 | fatal: colors.magenta 18 | }; 19 | if (process.env.THORIN_LOG_TIME === "false" || global.THORIN_LOG_TIME === 'false' || global.THORIN_LOG_TIME === false || process.env.KUBERNETES_SERVICE_HOST) { 20 | DATE_FORMAT = ""; 21 | } 22 | module.exports = function (thorin) { 23 | colors.enabled = true; 24 | let forceColors = global.THORIN_COLOR || process.env.THORIN_COLOR; 25 | if (!forceColors && (thorin.docker === true || global.THORIN_DOCKER)) { 26 | colors.enabled = false; 27 | } 28 | let globalLoggerName = "log", // default global.log() function name. 29 | consoleLogging = true, 30 | globalConsole = { 31 | log: global.console.log.bind(global.console) 32 | }; 33 | const loggerListeners = []; 34 | const loggerMap = { // a hash of all registered loggers. 35 | default: new ThorinLogger('default') 36 | }; 37 | 38 | /* This is what we expose with the thorin logger. We do not process 39 | * the actual log string, we just emit log events. */ 40 | function ThorinLogger(name, _opt) { 41 | this.name = name; 42 | } 43 | 44 | /* This is a shortcut for a log level caller. */ 45 | ThorinLogger.prototype.log = function DoLogWithLevel(level) { 46 | if (typeof level !== 'string') this; 47 | level = level.toLowerCase(); 48 | if (typeof this[level] !== 'function') return this; 49 | let args = Array.prototype.slice.call(arguments); 50 | args.splice(0, 1); 51 | return this[level].apply(this, args); 52 | }; 53 | 54 | LOG_LEVELS.forEach((level) => { 55 | if (global.console[level]) { 56 | globalConsole[level] = global.console[level].bind(global.console); 57 | } 58 | ThorinLogger.prototype[level] = function (msg) { 59 | let newArgs = Array.prototype.slice.call(arguments), 60 | errArgs = []; 61 | let logStr = '[' + level.toUpperCase() + '] ', 62 | timeStr = getDateFormat(), 63 | plainStr = ''; 64 | if (timeStr) { 65 | logStr += '[' + timeStr + '] '; 66 | } 67 | logStr += '[' + this.name + '] '; 68 | 69 | if (typeof msg !== 'string' || (typeof msg === 'string' && msg.indexOf('%') !== -1)) { 70 | plainStr = util.format.apply(util, arguments); 71 | logStr += plainStr; 72 | } else { 73 | plainStr = msg; 74 | // add any remaining strings. 75 | for (let i = 1; i < newArgs.length; i++) { 76 | if (typeof newArgs[i] === 'string' || typeof newArgs[i] === 'boolean' || typeof newArgs[i] === 'number') { 77 | plainStr += ' ' + newArgs[i]; 78 | } 79 | } 80 | logStr += plainStr; 81 | } 82 | logStr = LOG_COLORS[level](logStr); 83 | let consoleArgs = [logStr], 84 | hasConsoleError = false; 85 | for (let i = 0; i < newArgs.length; i++) { // spit out any non-strings. 86 | if (newArgs[i] instanceof Error) { 87 | hasConsoleError = true; 88 | if (newArgs[i].name.indexOf('Thorin') === 0 && newArgs[i].stack && (newArgs[i].statusCode == 500 || newArgs[i].ns == 'GLOBAL')) { 89 | consoleArgs.push(newArgs[i].stack); 90 | } else { 91 | errArgs.push(newArgs[i]); 92 | } 93 | } else if (typeof msg === 'string' && typeof newArgs[i] === 'object' && newArgs[i]) { 94 | consoleArgs.push(newArgs[i]); 95 | } 96 | } 97 | if (consoleLogging) { 98 | globalConsole.log.apply(globalConsole, consoleArgs); 99 | if (errArgs.length > 0 && thorin.env !== 'production' && !hasConsoleError) { 100 | globalConsole.trace.apply(globalConsole, errArgs); 101 | } 102 | } 103 | if (loggerListeners.length === 0) return; 104 | let item = { 105 | ts: Date.now(), 106 | name: this.name, 107 | message: plainStr, 108 | level: level, 109 | args: Array.prototype.slice.call(arguments) 110 | }; 111 | 112 | for (let i = 0; i < loggerListeners.length; i++) { 113 | let listener = loggerListeners[i]; 114 | if (typeof listener.name === 'string' && listener.name !== this.name) continue; 115 | try { 116 | listener.fn(item); 117 | } catch (e) { 118 | if (consoleLogging) { 119 | globalConsole.error('Thorin.logger: log listener for logger ' + this.name + ' threw an error.'); 120 | globalConsole.error(e); 121 | } 122 | } 123 | } 124 | } 125 | }); 126 | 127 | 128 | /* This will either create a new logger instance or fetch the default one. */ 129 | function logger(loggerName) { 130 | if (typeof loggerName === 'undefined') loggerName = 'default'; 131 | if (typeof loggerMap[loggerName] !== 'undefined') return loggerMap[loggerName]; 132 | const loggerObj = new ThorinLogger(loggerName); 133 | loggerMap[loggerName] = loggerObj; 134 | return loggerObj; 135 | } 136 | 137 | /* Disables all the console logging */ 138 | logger.disableConsole = function DisableConsoleLogging() { 139 | consoleLogging = false; 140 | return logger; 141 | }; 142 | /* Enables all console logging. */ 143 | logger.enableConsole = function EnableConsoleLogging() { 144 | consoleLogging = true; 145 | return logger; 146 | }; 147 | 148 | /* Adds an log event handler. */ 149 | logger.pipe = function PipeLogEvents(a, fn) { 150 | let loggerName, pipeFn, item = {}; 151 | if (typeof a === 'string' && typeof fn === 'function') { 152 | loggerName = a; 153 | pipeFn = fn; 154 | } else if (typeof a === 'function') { 155 | pipeFn = a; 156 | } 157 | if (typeof pipeFn !== 'function') { 158 | if (consoleLogging) { 159 | globalConsole.error('thorin.logger.pipe(): callback is not a function'); 160 | } 161 | } else { 162 | item.fn = pipeFn; 163 | if (loggerName) item.name = loggerName; 164 | loggerListeners.push(item); 165 | } 166 | return logger; 167 | }; 168 | 169 | /* Manually override the global var name */ 170 | logger.globalize = function UpdateGlobalName(name) { 171 | if (name === false && typeof global[globalLoggerName] !== 'undefined') { 172 | delete global[globalLoggerName]; 173 | return this; 174 | } 175 | if (typeof name === 'undefined') { 176 | name = globalLoggerName; 177 | } 178 | if (typeof global[globalLoggerName] !== 'undefined') { 179 | delete global[globalLoggerName]; 180 | } 181 | globalLoggerName = name; 182 | global[globalLoggerName] = loggerMap['default']; 183 | }; 184 | 185 | /* Replaces the console logger with our logger. */ 186 | logger.replaceConsole = function ReplaceConsoleLogger() { 187 | let defaultLogger = logger(); 188 | LOG_LEVELS.forEach((level) => { 189 | global.console[level] = defaultLogger[level].bind(defaultLogger); 190 | }); 191 | global.console.log = defaultLogger.info.bind(defaultLogger); 192 | }; 193 | 194 | return logger; 195 | 196 | }; 197 | 198 | function padWithZeros(vNumber, width) { 199 | var numAsString = vNumber + ""; 200 | while (numAsString.length < width) { 201 | numAsString = "0" + numAsString; 202 | } 203 | return numAsString; 204 | } 205 | 206 | function offset(timezoneOffset) { 207 | // Difference to Greenwich time (GMT) in hours 208 | var os = Math.abs(timezoneOffset); 209 | var h = String(Math.floor(os / 60)); 210 | var m = String(os % 60); 211 | if (h.length == 1) { 212 | h = "0" + h; 213 | } 214 | if (m.length == 1) { 215 | m = "0" + m; 216 | } 217 | return timezoneOffset < 0 ? "+" + h + m : "-" + h + m; 218 | } 219 | 220 | function addZero(vNumber) { 221 | return padWithZeros(vNumber, 2); 222 | } 223 | 224 | function getDateFormat() { 225 | var date = new Date(), 226 | timezoneOffset = date.getTimezoneOffset(); 227 | date.setUTCMinutes(date.getUTCMinutes() - timezoneOffset); 228 | var vDay = addZero(date.getUTCDate()); 229 | var vMonth = addZero(date.getUTCMonth() + 1); 230 | var vYearLong = addZero(date.getUTCFullYear()); 231 | var vYearShort = addZero(date.getUTCFullYear().toString().substring(2, 4)); 232 | var vYear = (DATE_FORMAT.indexOf("yyyy") > -1 ? vYearLong : vYearShort); 233 | var vHour = addZero(date.getUTCHours()); 234 | var vMinute = addZero(date.getUTCMinutes()); 235 | var vSecond = addZero(date.getUTCSeconds()); 236 | var vMillisecond = padWithZeros(date.getUTCMilliseconds(), 3); 237 | var vTimeZone = offset(timezoneOffset); 238 | date.setUTCMinutes(date.getUTCMinutes() + timezoneOffset); 239 | return DATE_FORMAT 240 | .replace(/dd/g, vDay) 241 | .replace(/MM/g, vMonth) 242 | .replace(/y{1,4}/g, vYear) 243 | .replace(/hh/g, vHour) 244 | .replace(/mm/g, vMinute) 245 | .replace(/ss/g, vSecond) 246 | .replace(/SSS/g, vMillisecond) 247 | .replace(/O/g, vTimeZone); 248 | } 249 | -------------------------------------------------------------------------------- /lib/interface/IModule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter; 3 | /** 4 | * Created by Adrian on 19-Mar-16. 5 | */ 6 | 7 | module.exports = class IModule extends EventEmitter { 8 | 9 | constructor() { 10 | super(); 11 | this.setMaxListeners(Infinity); 12 | this.name = "module"; 13 | } 14 | 15 | /* Should return an array of thorin dependencies, if any */ 16 | static dependencies() { 17 | return []; 18 | } 19 | 20 | /* Manually stop the module */ 21 | stop(done) { 22 | done(); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/interface/ISanitizer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Created by Adrian on 20-Mar-16. 4 | */ 5 | module.exports = class ISanitizer { 6 | static code() { return "DEFAULT"; } // this is the code of the sanitizer that will be attached to thorin.sanitize 7 | static publicName() { return "DEFAULT"; } // this is the user friendly name of the sanitizer. 8 | static aliases() { return []; } // if we have the same validator for multiple names, we can return an array of capital codes to map 9 | 10 | /* 11 | * The validate() function will be called with the input data. It must only 12 | * check if the input is of actual type and if it fits the input criteria. 13 | * IF the validation fails, it can return a falsy value (false, null, etc). 14 | * IF the input is valid, it MUST return an object containing: 15 | * IF the sanitizer is promise-based, it MUST return a promise and resolve with the actual result. 16 | * { 17 | * value: "sanitizedValue" 18 | * } 19 | * */ 20 | validate(input, opt) { 21 | throw new Error("Thorin.ISanitizer: " + this.code + " validate() not implemented."); 22 | } 23 | 24 | }; -------------------------------------------------------------------------------- /lib/interface/IStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter; 3 | /** 4 | * Created by Adrian on 19-Mar-16. 5 | */ 6 | 7 | module.exports = class IStore extends EventEmitter{ 8 | 9 | constructor() { 10 | super(); 11 | this.setMaxListeners(Infinity); 12 | this.type = "store"; // the store type. 13 | this.name = "store"; 14 | } 15 | 16 | static publicName() { return "store"; } 17 | 18 | /* Sets the name's instance. */ 19 | setName(name) { 20 | this.name = name; 21 | } 22 | 23 | /* Manually stop the store */ 24 | stop(done) { 25 | done(); 26 | } 27 | }; -------------------------------------------------------------------------------- /lib/interface/ITransport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter; 3 | /** 4 | * Created by Adrian on 19-Mar-16. 5 | */ 6 | 7 | class ITransport extends EventEmitter { 8 | 9 | constructor() { 10 | super(); 11 | this.setMaxListeners(Infinity); 12 | this.type = 2; // by default, we have receivers. 13 | this.name = 'transport'; 14 | } 15 | 16 | static publicName() { return "transport"; } 17 | 18 | /* Sets the name's instance */ 19 | setName(name) { 20 | this.name = name; 21 | } 22 | 23 | /* This is used for bi-directional/sender transports, and must be overridden, 24 | * with the own logic of sending events from the server to clients. */ 25 | sendIntent(intentObj) {} 26 | 27 | /* 28 | * This is called when we want to temporary disable an action from being processed. 29 | * */ 30 | disableAction(actionName) {} 31 | 32 | /* 33 | * This is called when we want to re-enable a disabled action 34 | * */ 35 | enableAction(actionName) {} 36 | 37 | /* 38 | * This is called whenever an intent is registered in the app. The transport 39 | * must then register its handler. 40 | * Example: 41 | * we have a HTTP transport that will bind to a port and listen to GET/POST requests. 42 | * handleIntent(iObj) will be called. the iObj contains information about the path and input data 43 | * - the server will have to attach an onSuccess and onError handlers to it, 44 | * - and redirect outgoing data through the http response. 45 | * - once a request matches an intent and has its incoming data processed, 46 | * the transport will then emit an "intent" event, with the incoming data, 47 | * authorization data and client information attached. The dispatcher 48 | * will handle the rest. 49 | * */ 50 | routeAction(actionObj) {} 51 | } 52 | /* 53 | * Transports can also allow bi-directional communication (server->client), therefore, the types of transport are: 54 | * 1. BI_DIRECTIONAL (listens for intents and implements a sendIntent() function) 55 | * 2. RECEIVER (only listens for intents) 56 | * 3. SENDER (only sends intents) 57 | * */ 58 | ITransport.TYPE = { 59 | BI_DIRECTIONAL: 1, 60 | RECEIVER: 2, 61 | SENDER: 3 62 | }; 63 | 64 | module.exports = ITransport; -------------------------------------------------------------------------------- /lib/routing/action.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Created by Adrian on 03-Apr-16. 4 | * 5 | * The Thorin Action will register prerequisits for an intent to be processed. 6 | * It can be viewed as the classical "Route", but with a fancy name. 7 | */ 8 | const async = require('async'); 9 | module.exports = function (thorin) { 10 | const PROXY_HANDLER_TYPE = 'proxy.self'; 11 | const HANDLER_TYPE = { 12 | AUTHORIZE: 'authorize', 13 | VALIDATE: 'validate', 14 | VALIDATE_FILTER: 'filter', 15 | MIDDLEWARE: 'middleware', 16 | USE: 'use', 17 | PROXY_SELF: 'proxy.self' 18 | }; 19 | 20 | const onRegisteredFns = Symbol(); 21 | 22 | class ThorinIntentAction { 23 | 24 | constructor(name) { 25 | this.hasDebug = true; 26 | this.isTemplate = false; 27 | // this[onRegisteredFns] = []; // an array of functions that will be called when the action was registered in the dispatcher. 28 | this.root = ""; // the root that is applied for aliases. 29 | this.name = name; 30 | this.aliases = []; 31 | this.stack = []; 32 | this.templates = []; // placed at first template() call 33 | this.ends = []; // an array of end fns to call when the intent finalizes. 34 | this.events = { 35 | before: {}, // a hash of {handlerType: [fns]} 36 | after: {} // same as before 37 | }; 38 | } 39 | 40 | /* 41 | * Disables any kind of debugging for this action 42 | * */ 43 | debug(v) { 44 | this.hasDebug = (typeof v === 'boolean' ? v : false); 45 | return this; 46 | } 47 | 48 | /* 49 | * Sets an alias to this action. 50 | * An alias will be typically used by the HTTP transport to 51 | * map url requests to this action. 52 | * NOTE: 53 | * template actions that call the alias() will set the root alias name 54 | * of the template action. Any other actions that extend this template 55 | * will have their aliases prepended by this. 56 | * */ 57 | alias(verb, name) { 58 | if (this.isTemplate === true) { 59 | if (typeof verb !== 'string') { 60 | console.error('Thorin.alias: root alias of template must be a string for template ' + this.name); 61 | return this; 62 | } 63 | this.root = verb; 64 | } else { 65 | if (typeof verb === 'string' && typeof name === 'undefined') { // we have only name-based aliases. 66 | this.aliases.push({ 67 | name: verb 68 | }); 69 | return this; 70 | } 71 | if (typeof verb !== 'string') { 72 | console.error('Thorin.alias: verb and alias must be a string for action ' + this.name); 73 | return this; 74 | } 75 | this.aliases.push({ 76 | verb, 77 | name 78 | }); 79 | } 80 | return this; 81 | } 82 | 83 | /* 84 | * Registers an authorization handler. 85 | * Authorization handlers are registered through dispatcher.addAuthorization 86 | * and are named ones. The action authorize() function works only with strings. 87 | * Usage: 88 | * actionObj.authorize('some.authorization', {options}) 89 | * - OPTIONAL: if a conditionFn is specified, the authorization will be executed only when the result of the conditional function is true. 90 | * */ 91 | authorize(authName, opt, conditionFn) { 92 | if (typeof authName !== 'string') { 93 | console.error('Thorin.action: authorization ' + authName + " of action " + this.name + ' is not a string.'); 94 | return this; 95 | } 96 | if (authName instanceof Array) { 97 | for (let i = 0; i < authName.length; i++) { 98 | if (typeof authName[i] !== 'string') continue; 99 | let item = { 100 | type: HANDLER_TYPE.AUTHORIZE, 101 | name: authName[i], 102 | opt: {} 103 | }; 104 | if (typeof conditionFn === 'function') item.condition = conditionFn; 105 | this.stack.push(item); 106 | } 107 | return this; 108 | } 109 | let item = { 110 | type: HANDLER_TYPE.AUTHORIZE, 111 | name: authName, 112 | opt: (typeof opt === 'undefined' ? {} : opt) 113 | }; 114 | if (typeof conditionFn === 'function') item.condition = conditionFn; 115 | this.stack.push(item); 116 | return this; 117 | } 118 | 119 | /* ALIAS to authorize() */ 120 | authorization() { 121 | return this.authorize.apply(this, arguments); 122 | } 123 | 124 | /* 125 | * Registers a "BEFORE" handler. 126 | * Before handlers are synchronous functions that are called before specific 127 | * points in the execution stack. Typically, we have a before(HANDLER_TYPE, fn) 128 | * Usage: 129 | * actionObj.before('validate', (intentObj) => {}) 130 | * actionObj.before('middleware', 'middlewareName', () => {}) 131 | * */ 132 | before(type, a, b) { 133 | return addHandler.call(this, 'before', type, a, b); 134 | } 135 | 136 | after(type, a, b) { 137 | return addHandler.call(this, 'after', type, a, b); 138 | } 139 | 140 | /* 141 | * Adds a new input data validator. 142 | * Usage: 143 | * actionObj.input({ 144 | * myKey: dispatcher.sanitize("STRING") 145 | * }) 146 | * */ 147 | input(obj) { 148 | if (typeof obj !== 'object' || !obj) { 149 | console.error('Thorin.action: validator must be a key-value object for action ' + this.name); 150 | return this; 151 | } 152 | let item = { 153 | type: HANDLER_TYPE.VALIDATE, 154 | value: obj 155 | }; 156 | this.stack.push(item); 157 | return this; 158 | } 159 | 160 | /* 161 | * Since some requests (specifically UPDATE requests) can rely on two differente input payloads, 162 | * one for filtering, the other for raw data, the input can now also have filter() data. 163 | * By default, the incoming data will be: 164 | * { 165 | * "type": "my.action", 166 | * "filter": { 167 | * "id": "1" 168 | * }, 169 | * "payload": { 170 | * "name": "John" 171 | * } 172 | * } 173 | * // use the filter() in WHERE statements when querying for instances 174 | * // use input() for actual data to work with. 175 | * // NOTE: validation works exactly like in the input() part. 176 | * */ 177 | filter(obj) { 178 | if (typeof obj !== 'object' || !obj) { 179 | console.error('Thorin.action: validator must be a key-value object for action ' + this.name); 180 | return this; 181 | } 182 | let item = { 183 | type: HANDLER_TYPE.VALIDATE_FILTER, 184 | value: obj 185 | }; 186 | this.stack.push(item); 187 | return this; 188 | } 189 | 190 | /* 191 | * Performs internal proxying from one action to another. 192 | * OPTIONS: 193 | * - action=string -> the target namespace 194 | * - payload=object -> the base payload that will override the intent input. 195 | * - rawInput=false -> should we use intentObj.input() or intentObj.rawInput 196 | * - exclude: [], -> array of keys to exclude from input 197 | * */ 198 | proxy(proxyServiceName, opt) { 199 | if (typeof proxyServiceName !== 'string' || !proxyServiceName) { 200 | log.error(`proxy() of action ${this.name} must have a valid string for the proxy service name`); 201 | return this; 202 | } 203 | let tmp = proxyServiceName.split('#'), 204 | proxyName = tmp[0], 205 | serviceName = tmp[1]; 206 | if (proxyName !== 'self') { 207 | if (typeof super.proxy === 'function') { 208 | return super.proxy.apply(this, arguments); 209 | } 210 | log.warn(`proxy() must contain the following pattern: self#{actionName} [current: ${proxyServiceName}]`); 211 | return this; 212 | } 213 | let options = Object.assign({}, { 214 | action: serviceName, 215 | rawInput: true, 216 | exclude: [], 217 | payload: {} 218 | }, opt || {}); 219 | this.stack.push({ 220 | name: proxyServiceName, 221 | type: HANDLER_TYPE.PROXY_SELF, 222 | opt: options 223 | }); 224 | return this; 225 | } 226 | 227 | /* Proxy function for middleware type. */ 228 | middleware(fn, a) { 229 | this.use(fn, a); 230 | return this; 231 | } 232 | 233 | /* 234 | * This handler is the one that should be particular to the action. 235 | * An action can use() 236 | * - an array of middleware names (with no options passed to them) 237 | * - a middleware name and pass the middleware options to it 238 | * - a callback function that will be used within the action handler. 239 | * - OPTIONAL: if a conditionFn is specified, the middleware will be executed only when the result of the conditional function is true. 240 | * Usage: 241 | * actionObj.use('my.middleware', {withMy: 'options'}) 242 | * actionObj.use(['my:middleware', 'some.other.middleware'] 243 | * actionObj.use((intentObj) => {}); 244 | * */ 245 | use(fn, a, conditionFn) { 246 | if (typeof fn === 'function') { // use an fn() 247 | let item = { 248 | type: HANDLER_TYPE.USE, 249 | fn: fn 250 | }; 251 | if (typeof a === 'object' && a) { 252 | item.opt = a; 253 | } 254 | if (typeof conditionFn === 'function') item.condition = conditionFn; 255 | this.stack.push(item); 256 | return this; 257 | } 258 | if (fn instanceof Array) { // array of middleware names. 259 | for (let i = 0; i < fn.length; i++) { 260 | if (typeof fn[i] === 'string') { 261 | let item = { 262 | type: HANDLER_TYPE.MIDDLEWARE, 263 | name: fn[i], 264 | opt: {} 265 | }; 266 | if (typeof conditionFn === 'function') item.condition = conditionFn; 267 | this.stack.push(item); 268 | } 269 | } 270 | return this; 271 | } 272 | if (typeof fn === 'string') { // a middleware name with options? 273 | let item = { 274 | type: HANDLER_TYPE.MIDDLEWARE, 275 | name: fn, 276 | opt: (typeof a === 'undefined' ? {} : a) 277 | }; 278 | if (typeof conditionFn === 'function') item.condition = conditionFn; 279 | this.stack.push(item); 280 | return this; 281 | } 282 | console.warn('Thorin.action: invalid usage of use() for action ' + this.name); 283 | return this; 284 | } 285 | 286 | /* 287 | * Registers an end callback. Similar to the ones in middleware, 288 | * an end callback will be called whenever the intent will complete. 289 | * */ 290 | end(fn) { 291 | if (typeof fn !== 'function') { 292 | console.error('Thorin.action: invalid function for end() for action ' + this.name); 293 | return this; 294 | } 295 | this.ends.push(fn); 296 | return this; 297 | } 298 | 299 | /* 300 | * This function is called when the dispatcher has registered the action, AFTER 301 | * it was extended from its parent template (if any). 302 | * */ 303 | onRegister(fn) { 304 | if (!this[onRegisteredFns]) this[onRegisteredFns] = []; 305 | this[onRegisteredFns].push(fn); 306 | return this; 307 | } 308 | 309 | /* 310 | * This is called by the dispatcher when it was registered. 311 | * */ 312 | _register() { 313 | if (!this[onRegisteredFns]) return; 314 | for (let i = 0; i < this[onRegisteredFns].length; i++) { 315 | try { 316 | this[onRegisteredFns][i].call(this, this); 317 | } catch (e) { 318 | console.error('Thorin action ' + this.name + ' caught an error in onRegister()', e); 319 | } 320 | } 321 | delete this[onRegisteredFns]; 322 | } 323 | 324 | /* 325 | * Plugins or other components can actually insert functionality into thorin.Action. 326 | * All they have to do is override the "_runCustomType" function of the action 327 | * and whenever a custom action that is not in the default handler types will be registered, 328 | * it will be processed. 329 | * 330 | * */ 331 | _runCustomType(intentObj, handler, done) { 332 | if (handler.type !== HANDLER_TYPE.PROXY_SELF) { 333 | return done(); 334 | } 335 | let opt = handler.opt, 336 | action = opt.action, 337 | intentInput = {}; 338 | if (opt.rawInput === true || typeof opt.fields === 'object' && opt.fields) { 339 | intentInput = intentObj.rawInput; 340 | } else { 341 | intentInput = intentObj.input(); 342 | } 343 | let payload = opt.payload ? JSON.parse(JSON.stringify(opt.payload)) : {}; 344 | if (typeof opt.fields === 'object' && opt.fields) { 345 | Object.keys(opt.fields).forEach((keyName) => { 346 | if (typeof intentInput[keyName] === 'undefined') return; 347 | let newKeyName = opt.fields[keyName]; 348 | if (newKeyName === true) { 349 | payload[keyName] = intentInput[keyName]; 350 | } else if (typeof newKeyName === 'string') { 351 | payload[newKeyName] = intentInput[keyName]; 352 | } 353 | }); 354 | } else { 355 | payload = Object.assign({}, intentInput, opt.payload); 356 | } 357 | 358 | if (opt.exclude instanceof Array) { 359 | for (let i = 0; i < opt.exclude.length; i++) { 360 | let keyName = opt.exclude[i]; 361 | if (typeof payload[keyName] !== 'undefined') delete payload[keyName]; 362 | } 363 | } 364 | 365 | this._runHandler( 366 | 'before', 367 | PROXY_HANDLER_TYPE, 368 | intentObj, 369 | action, 370 | payload 371 | ); 372 | thorin.dispatcher.dispatch(action, payload, intentObj, true).then((res) => { 373 | if (typeof res.meta !== 'undefined') { 374 | intentObj.setMeta(res.meta); 375 | } 376 | if (typeof res.result !== 'undefined') { 377 | intentObj.result(res.result); 378 | } 379 | }).catch((e) => { 380 | intentObj.error(thorin.error(e.error || e)); 381 | this._runHandler( 382 | 'after', 383 | PROXY_HANDLER_TYPE, 384 | intentObj, 385 | action, 386 | payload 387 | ); 388 | }).finally(() => { 389 | done(); 390 | }); 391 | } 392 | 393 | /* 394 | * The first thing we do when an intent is incoming, we have to run all its 395 | * stack. 396 | * */ 397 | _runStack(intentObj, onComplete) { 398 | let calls = []; 399 | this.stack.forEach((item) => { 400 | /* Check the FILTER handler */ 401 | if (item.type === HANDLER_TYPE.VALIDATE_FILTER) { 402 | calls.push((done) => { 403 | if (intentObj.completed) return done(); // skip, we completed. 404 | this._runHandler('before', HANDLER_TYPE.VALIDATE_FILTER, intentObj, null, item.value); 405 | thorin.dispatcher.validateIntentFilter(intentObj, item.value, (e) => { 406 | this._runHandler('after', HANDLER_TYPE.VALIDATE_FILTER, intentObj, null, item.value, e); 407 | done(e); 408 | }); 409 | }); 410 | return; 411 | } 412 | /* Check the VALIDATE handler. */ 413 | if (item.type === HANDLER_TYPE.VALIDATE) { 414 | calls.push((done) => { 415 | if (intentObj.completed) return done(); // skip, we completed. 416 | this._runHandler('before', HANDLER_TYPE.VALIDATE, intentObj, null, item.value); 417 | thorin.dispatcher.validateIntent(intentObj, item.value, (e) => { 418 | this._runHandler('after', HANDLER_TYPE.VALIDATE, intentObj, null, item.value, e); 419 | done(e); 420 | }); 421 | }); 422 | return; 423 | } 424 | /* Check the MIDDLEWARE handler*/ 425 | if (item.type === HANDLER_TYPE.MIDDLEWARE) { 426 | let middlewareObj = thorin.dispatcher.getMiddleware(item.name); 427 | if (!middlewareObj) { 428 | console.error('Thorin.action._runStack: dispatcher does not have a middleware called ' + item.name + ' for action ' + this.name); 429 | return; 430 | } 431 | calls.push((done) => { 432 | if (intentObj.completed) return done(); // skip, we completed. 433 | /* CHECK if we should run the middleware (if it has a condition) */ 434 | if (typeof item.condition === 'function') { 435 | let shouldRun; 436 | try { 437 | shouldRun = item.condition(intentObj); 438 | } catch (e) { 439 | console.error('Thorin.action._runStack: use(' + item.fn.name + ') function threw an error in middleware condition of action ' + this.name); 440 | return done(); 441 | } 442 | 443 | if (shouldRun !== true) return done(); 444 | } 445 | this._runHandler('before', HANDLER_TYPE.MIDDLEWARE, intentObj, item.name); 446 | middlewareObj._runStack(intentObj, cloneOpt(item.opt), (e) => { 447 | this._runHandler('after', HANDLER_TYPE.MIDDLEWARE, intentObj, item.name, e); 448 | done(e); 449 | }); 450 | }); 451 | return; 452 | } 453 | 454 | /* Check the AUTHORIZE handler */ 455 | if (item.type === HANDLER_TYPE.AUTHORIZE) { 456 | let authObj = thorin.dispatcher.getAuthorization(item.name); 457 | if (!authObj) { 458 | console.error('Thorin.action._runStack: dispatcher does not have an authorization called ' + item.name + ' for action ' + this.name); 459 | return; 460 | } 461 | calls.push((done) => { 462 | if (intentObj.completed) return done(); // skip, we completed. 463 | /* CHECK if we should run the middleware (if it has a condition) */ 464 | if (typeof item.condition === 'function') { 465 | let shouldRun; 466 | try { 467 | shouldRun = item.condition(intentObj); 468 | } catch (e) { 469 | console.error('Thorin.action._runStack: use(' + item.fn.name + ') function threw an error in authorization condition of action ' + this.name); 470 | return done(); 471 | } 472 | if (shouldRun !== true) return done(); 473 | } 474 | this._runHandler('before', HANDLER_TYPE.AUTHORIZE, intentObj, item.name); 475 | authObj._runStack(intentObj, cloneOpt(item.opt), (e) => { 476 | this._runHandler('after', HANDLER_TYPE.AUTHORIZE, intentObj, item.name, e); 477 | done(e); 478 | }); 479 | }); 480 | return; 481 | } 482 | 483 | /* check the USE functionality */ 484 | if (item.type === HANDLER_TYPE.USE) { 485 | let wasCallCompleted = false; 486 | calls.push((done) => { 487 | if (intentObj.completed) return done(); // skip, we completed. 488 | // when the intent ends or when the first next() is called, we stop this call. 489 | function doneWrap(e) { 490 | if (wasCallCompleted) return; 491 | wasCallCompleted = true; 492 | done(e); 493 | } 494 | 495 | intentObj.on('end', doneWrap); 496 | try { 497 | item.fn(intentObj, doneWrap, cloneOpt(item.opt)); 498 | } catch (e) { 499 | console.error('Thorin.action._runStack: use(' + item.fn.name + ') function threw an error in action ' + this.name); 500 | console.error(e.stack); 501 | doneWrap(thorin.error(e)); 502 | } 503 | }); 504 | return; 505 | } 506 | 507 | /* Otherwise, we have a different kind of type that was inserted by a plugin. */ 508 | calls.push((done) => { 509 | this._runCustomType(intentObj, item, done); 510 | }); 511 | }); 512 | async.series(calls, (err) => { 513 | calls = null; 514 | intentObj.on('end', () => { 515 | for (let i = 0; i < this.ends.length; i++) { 516 | try { 517 | this.ends[i](intentObj); 518 | } catch (e) { 519 | console.error('Thorin.action: end() callback threw an error in action ' + this.name, this.ends[i]); 520 | console.error(e); 521 | } 522 | } 523 | }); 524 | onComplete(err); 525 | }); 526 | } 527 | 528 | /* 529 | * Triggers a before() registered callback for the given event and intentObj. 530 | * */ 531 | 532 | /* The template() function is overridden by the dispatcher, so that it 533 | * can control how to extend an action with its template */ 534 | template(name) { 535 | this.templates.push(name); 536 | return this; 537 | } 538 | 539 | /* 540 | * This function is called whenever this action wants to include stuff 541 | * from another action template. 542 | * */ 543 | _extendFromParent(parentObj) { 544 | // extend stack 545 | this.stack = parentObj.stack.concat(this.stack); 546 | this.ends = parentObj.ends.concat(this.ends); 547 | if (this.hasDebug !== false) { 548 | this.hasDebug = parentObj.hasDebug; 549 | } 550 | // extend before events. 551 | Object.keys(parentObj.events.before).forEach((name) => { 552 | if (typeof this.events.before[name] === 'undefined') { 553 | this.events.before[name] = []; 554 | } 555 | this.events.before[name] = parentObj.events.before[name].concat(this.events.before[name]); 556 | }); 557 | // extend after events. 558 | Object.keys(parentObj.events.after).forEach((name) => { 559 | if (typeof this.events.after[name] === 'undefined') { 560 | this.events.after[name] = []; 561 | } 562 | this.events.after[name] = parentObj.events.after[name].concat(this.events.after[name]); 563 | }); 564 | // extend the root, if any. 565 | let fullRoot = this.root; 566 | if (parentObj.root !== '') { 567 | let beforeRoot = parentObj.root; 568 | if (beforeRoot.charAt(beforeRoot.length - 1) === '/') { 569 | beforeRoot = beforeRoot.substr(0, beforeRoot.length - 1); 570 | } 571 | fullRoot = beforeRoot + fullRoot; 572 | if (fullRoot.charAt(fullRoot.length - 1) === '/') fullRoot = fullRoot.substr(0, fullRoot.length - 1); 573 | this.root = fullRoot; 574 | } 575 | if (fullRoot !== '') { 576 | for (let i = 0; i < this.aliases.length; i++) { 577 | let item = this.aliases[i]; 578 | // CHECK if we have "/" in the name. If we do, we have to normalize the path. 579 | if (item.name.charAt(0) !== '/') item.name = '/' + item.name; 580 | this.aliases[i].name = fullRoot + item.name; 581 | } 582 | } 583 | // extend exposed req/res if any 584 | if (parentObj.exposeRequest === true) this.exposeRequest = true; 585 | if (parentObj.exposeResponse === true) this.exposeResponse = true; 586 | if (parentObj.aliasOnly) this.aliasOnly = true; 587 | } 588 | 589 | /* 590 | * This will run the given event handler, if any fn was registered for it. 591 | * */ 592 | _runHandler(handlerType, eventName, intentObj, subName, _arg1, _arg2) { 593 | if (typeof this.events[handlerType][eventName] === 'undefined') return; 594 | for (let i = 0; i < this.events[handlerType][eventName].length; i++) { 595 | let item = this.events[handlerType][eventName][i]; 596 | if (typeof subName === 'string') { 597 | if (typeof item.name === 'string' && item.name !== subName) continue; 598 | } 599 | try { 600 | item.fn(intentObj, _arg1, _arg2); 601 | } catch (e) { 602 | console.error('Thorin.action: ' + handlerType + '() called on ' + eventName + (subName ? '[' + subName + ']' : '') + ' caught an error in action ' + this.name); 603 | console.error(e); 604 | } 605 | } 606 | } 607 | } 608 | 609 | /* Adds an event handler, either a before or an after */ 610 | function addHandler(handlerType, type, a, b) { 611 | if (typeof type !== 'string') { 612 | console.error('Thorin.action: ' + handlerType + ' type ' + type + " of action " + this.name + ' is not a string.'); 613 | return this; 614 | } 615 | var item; 616 | if (type === HANDLER_TYPE.MIDDLEWARE || type === HANDLER_TYPE.AUTHORIZE) { // we have type, name, fn 617 | if (typeof a !== 'string' || typeof b !== 'function') { 618 | console.error('Thorin.action: ' + handlerType + ' middleware "' + a + '" must have syntax: ' + handlerType + '(type, middlewareName, fn) in action ' + this.name); 619 | return this; 620 | } 621 | item = { 622 | name: a, 623 | fn: b 624 | }; 625 | } else { 626 | item = {}; 627 | if (typeof a === 'function') { 628 | item.fn = a; 629 | } else if (typeof a === 'string' && typeof b === 'function') { 630 | item.name = a; 631 | item.fn = b; 632 | } 633 | if (typeof item.fn !== 'function') { 634 | console.error('Thorin.action: ' + handlerType + ' "' + type + '" must have syntax: ' + handlerType + '(type, fn) or (type, targetName, fn) in action ' + this.name); 635 | return this; 636 | } 637 | } 638 | if (typeof this.events[handlerType][type] === 'undefined') { 639 | this.events[handlerType][type] = []; 640 | } 641 | this.events[handlerType][type].push(item); 642 | return this; 643 | } 644 | function cloneOpt(opt) { 645 | try { 646 | if(typeof opt !== 'object' || !opt) return {}; 647 | let keys = Object.keys(opt); 648 | for (let i = 0; i < keys.length; i++) { 649 | let k = keys[i]; 650 | if (typeof opt[k] === 'function') { 651 | return opt; 652 | } 653 | } 654 | return JSON.parse(JSON.stringify(opt)); 655 | } catch (e) { 656 | return opt; 657 | } 658 | } 659 | ThorinIntentAction.HANDLER_TYPE = HANDLER_TYPE; 660 | return ThorinIntentAction; 661 | }; 662 | -------------------------------------------------------------------------------- /lib/routing/authorization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const async = require('async'); 3 | /** 4 | * Created by Adrian on 03-Apr-16. 5 | * 6 | * This is a thorin Authorization class. Its sole purpose is to callback with error, or attach 7 | * some data in the intent. 8 | * code. 9 | */ 10 | module.exports = function (thorin) { 11 | 12 | const use = Symbol(), 13 | validate = Symbol(), 14 | filter = Symbol(), 15 | end = Symbol(); 16 | 17 | class ThorinAuthorization { 18 | 19 | constructor(name) { 20 | this.name = name; 21 | this[validate] = []; // array of validations to the intent input 22 | this[filter] = [] // array of filter validations 23 | this[use] = []; // an array of functions to use. 24 | this[end] = []; // array of end symbols. 25 | } 26 | 27 | get stack() { 28 | return this[use]; 29 | } 30 | 31 | get validate() { 32 | return this[validate]; 33 | } 34 | 35 | get _filter() { 36 | return this[filter]; 37 | } 38 | 39 | /* 40 | * Register an intent input validator. 41 | * A validator item must be an object, and its keys are the intent's input keys. 42 | * */ 43 | input(item) { 44 | if (typeof item === 'object' && item) { 45 | this[validate].push(item); 46 | } 47 | return this; 48 | } 49 | 50 | /* 51 | * Register an intent filter validator. 52 | * A validator item must be an object, and its keys are the intent's input keys. 53 | * */ 54 | filter(item) { 55 | if (typeof item === 'object' && item) { 56 | this[filter].push(item); 57 | } 58 | return this; 59 | } 60 | 61 | /* 62 | * Registers a use callback. Use callbacks must be pure functions, and not string.s 63 | * */ 64 | use(fn) { 65 | if (typeof fn !== 'function') { 66 | console.error('Thorin.Authorization: use() function ' + fn + ' must be a function'); 67 | } else { 68 | this[use].push(fn); 69 | } 70 | return this; 71 | } 72 | 73 | end(fn) { 74 | if (typeof fn !== 'function') { 75 | console.error('Thorin.Authorization: end() function ' + fn + ' must be a function'); 76 | } else { 77 | this[end].push(fn); 78 | } 79 | return this; 80 | } 81 | 82 | /* 83 | * Runs the use function stack with the given intent. 84 | * */ 85 | _runStack(intentObj, opt, onDone) { 86 | let calls = []; 87 | intentObj.on('end', () => { 88 | for (let i = 0; i < this[end].length; i++) { 89 | try { 90 | this[end][i](intentObj); 91 | } catch (e) { 92 | console.error('Thorin.authorization: end() callback threw an error in authorization ' + this.name, this[end][i]); 93 | console.error(e); 94 | } 95 | } 96 | }); 97 | /* step one, for each validation, we have to include it in the calls. */ 98 | this[filter].forEach((item) => { 99 | calls.push((done) => { 100 | if (intentObj.completed) return done(); 101 | thorin.dispatcher.validateIntentFilter(intentObj, item, done); 102 | }); 103 | }); 104 | this[validate].forEach((item) => { 105 | calls.push((done) => { 106 | if (intentObj.completed) return done(); 107 | thorin.dispatcher.validateIntent(intentObj, item, done); 108 | }); 109 | }); 110 | this[use].forEach((fn) => { 111 | calls.push((done) => { 112 | try { 113 | fn(intentObj, done, cloneOpt(opt)); 114 | } catch (e) { 115 | console.error('Thorin.action.runStack: use(' + fn.name + ') function threw an error in authorization ' + this.name); 116 | console.trace(e); 117 | return done(thorin.error(e)); 118 | } 119 | }); 120 | }); 121 | async.series(calls, onDone); 122 | } 123 | 124 | } 125 | 126 | function cloneOpt(opt) { 127 | try { 128 | let keys = Object.keys(opt); 129 | for (let i = 0; i < keys.length; i++) { 130 | let k = keys[i]; 131 | if (typeof opt[k] === 'function') { 132 | return opt; 133 | } 134 | } 135 | return JSON.parse(JSON.stringify(opt)); 136 | } catch (e) { 137 | return opt; 138 | } 139 | } 140 | 141 | return ThorinAuthorization; 142 | }; 143 | -------------------------------------------------------------------------------- /lib/routing/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter, 3 | async = require('async'), 4 | initValidator = require('./validator'); 5 | /** 6 | * Created by Adrian on 03-Apr-16. 7 | * The Thorin dispatcher can be viewed as a mini router. It will 8 | * handle intent creation, sending and receiving. 9 | * It is similar to the dispatcher in flux. 10 | * EVENTS EMITTED: 11 | * middleware -> when a middleware was added 12 | * action -> when an action was added 13 | * transport -> when a transport was added 14 | * 15 | * intent -> when an intent was completed. 16 | */ 17 | 18 | module.exports = function init(thorin) { 19 | 20 | const eventerObj = new EventEmitter(); // we use this for internal event firing. 21 | eventerObj.setMaxListeners(Infinity); 22 | const transports = Symbol(), 23 | tracking = Symbol(), 24 | actions = Symbol(), 25 | templates = Symbol(), 26 | templatesPending = Symbol(), 27 | middleware = Symbol(), 28 | authorizations = Symbol(), 29 | IntentValidator = initValidator(thorin); 30 | 31 | const unsavedActions = []; //array of {action, transport} 32 | 33 | let transportId = 0, 34 | actionId = 0; 35 | 36 | class ThorinDispatcher extends EventEmitter { 37 | constructor() { 38 | super(); 39 | this.setMaxListeners(Infinity); 40 | this[templatesPending] = {}; 41 | this[templates] = {}; // a hash of already defined action templates. 42 | this[middleware] = {}; // a hash of {middlewareName: middlewareObj} 43 | this[authorizations] = {}; // a hash of authorizations. 44 | this[transports] = []; // an array of transports that were registered 45 | this[actions] = {}; // a hash of {action.name, action} 46 | this[tracking] = {}; // a hash of actionId:transportId to keep track of who was registered where. 47 | this.started = false; 48 | } 49 | 50 | /* Expose our actions, but as an array rather than a hash */ 51 | get actions() { 52 | let items = []; 53 | Object.keys(this[actions]).forEach((name) => items.push(this[actions][name].action)); 54 | return items; 55 | } 56 | 57 | /* 58 | * Registers a transport. Transports are used to intercept dispatcher intents. 59 | * */ 60 | registerTransport(transportObj) { 61 | if (!(transportObj instanceof thorin.Interface.Transport)) { 62 | console.error('Thorin.dispatcher.registerTransport: transport does not extend thorin.Interface.Transport'); 63 | return this; 64 | } 65 | this[transports].push(transportObj); 66 | this.emit('transport', transportObj); 67 | transportObj._id = transportId; 68 | transportId++; 69 | // We have to let the action get populated with stuff. 70 | if (this.started) { 71 | attachActionsToTransport.call(this, transportObj); 72 | } 73 | return this; 74 | } 75 | 76 | /* 77 | * Registers a template action. Template actions can be used as actions 78 | * that can be extended by the ones using the template. 79 | * Note: templates are always loaded first because they are the first ones 80 | * that can be extended, so they need to have all their information loaded. 81 | * */ 82 | addTemplate(actionObj) { 83 | if (typeof actionObj === 'string') { 84 | actionObj = new thorin.Action(actionObj); 85 | } 86 | if (!(actionObj instanceof thorin.Action)) { 87 | console.error('Thorin.dispatcher.addTemplate: template action is not an instance of thorin.Action'); 88 | return this; 89 | } 90 | if (typeof this[templates][actionObj.name] !== 'undefined') { 91 | console.error('Thorin.dispatcher.addTemplate: template action ' + actionObj.name + " is already defined."); 92 | return this; 93 | } 94 | actionObj.isTemplate = true; 95 | // We have to wait for the action to register its templates. 96 | process.nextTick(checkActionTemplates.bind(this, actionObj, () => { 97 | this.emit('template', actionObj); 98 | })); 99 | return actionObj; 100 | } 101 | 102 | /* 103 | * Registers a new action and attaches it to transports. 104 | * ACTION OPTIONS: 105 | * transport -> will attach the action only on the given transport. 106 | * save -> if set to false, we will not save the action. 107 | * */ 108 | addAction(actionObj, opt) { 109 | if (typeof opt !== 'object') opt = {}; 110 | if (typeof actionObj === 'string') { 111 | actionObj = new thorin.Action(actionObj); 112 | } 113 | if (!(actionObj instanceof thorin.Action) && typeof actionObj.use !== 'function') { 114 | console.error('Thorin.dispatcher.addAction: action is not an instance of thorin.Action'); 115 | return this; 116 | } 117 | /* Transports can alter the default options of an action */ 118 | if (typeof actionObj.getCustomOptions === 'function') { 119 | opt = actionObj.getCustomOptions(opt); 120 | delete actionObj.getCustomOptions; 121 | } 122 | if (opt.save !== false && typeof this[actions][actionObj.name] !== 'undefined') { 123 | console.error('Thorin.dispatcher.addAction: action ' + actionObj.name + ' already exists.'); 124 | return actionObj; 125 | } 126 | actionObj._id = actionId; 127 | actionId++; 128 | if (opt.save !== false) { 129 | this[actions][actionObj.name] = { 130 | action: actionObj, 131 | opt: opt 132 | }; 133 | } 134 | // We have to wait for the action to register its templates. 135 | process.nextTick(checkActionTemplates.bind(this, actionObj, () => { 136 | // If we're started, we have to attach the action to the transport. 137 | if (opt.save !== false) { 138 | for (let i = 0; i < this[transports].length; i++) { 139 | let transportObj = this[transports][i]; 140 | if (opt.transport && opt.transport !== transportObj.name) continue; 141 | let itm = {}; 142 | itm[actionObj.name] = true; 143 | attachActionsToTransport.call(this, transportObj, itm); 144 | } 145 | } else if (typeof opt.transport === 'string') { 146 | unsavedActions.push({ 147 | action: actionObj, 148 | opt 149 | }); 150 | for (let i = 0; i < this[transports].length; i++) { 151 | let transportObj = this[transports][i]; 152 | if (transportObj.name !== opt.transport) continue; 153 | let k = transportObj._id + ':' + actionObj._id; 154 | if (typeof this[tracking][k] !== 'undefined') continue; 155 | transportObj.routeAction(actionObj.name); 156 | this[tracking][k] = true; 157 | } 158 | } 159 | 160 | actionObj._register(); 161 | this.emit('action', actionObj); 162 | })); 163 | return actionObj; 164 | } 165 | 166 | /* 167 | * Registers a new middleware. 168 | * */ 169 | addMiddleware(middlewareObj) { 170 | if (typeof middlewareObj === 'string') { 171 | middlewareObj = new thorin.Middleware(middlewareObj); 172 | if (typeof this[middleware][middlewareObj.name] !== 'undefined') { 173 | console.error('Thorin.addMiddleware: middleware already exists: ' + middlewareObj.name); 174 | } else { 175 | this[middleware][middlewareObj.name] = middlewareObj; 176 | this.emit('middleware', middlewareObj); 177 | } 178 | return middlewareObj; 179 | } 180 | if (!(middlewareObj instanceof thorin.Middleware)) { 181 | console.error('Thorin.addMiddleware: middleware is not an instance of thorin.Middleware'); 182 | return this; 183 | } 184 | if (typeof this[middleware][middlewareObj.name] !== 'undefined') { 185 | console.error('Thorin.addMiddleware: middleware already exists: ' + middlewareObj.name); 186 | return this; 187 | } 188 | this[middleware][middlewareObj.name] = middlewareObj; 189 | this.emit('middleware', middlewareObj); 190 | return this; 191 | } 192 | 193 | /* 194 | * Registers a new authorization object to the dispatcher. 195 | * Authorizations have their sole purpose to verify if the 196 | * incoming intent has access to the requested action. 197 | * */ 198 | addAuthorization(authObj) { 199 | if (typeof authObj === 'string') { 200 | authObj = new thorin.Authorization(authObj); 201 | } 202 | if (!(authObj instanceof thorin.Authorization)) { 203 | console.error('Thorin.addAuthorization: authorization is not an instance of thorin.Authorization'); 204 | return this; 205 | } 206 | if (typeof this[authorizations][authObj.name] !== 'undefined') { 207 | console.error('Thorin.addAuthorization: authorization already exists: ' + authObj.name); 208 | } else { 209 | this[authorizations][authObj.name] = authObj; 210 | this.emit('authorization', authObj); 211 | } 212 | return authObj; 213 | } 214 | 215 | /* Returns the requested middleware object. */ 216 | getMiddleware(name) { 217 | return this[middleware][name] || null; 218 | } 219 | 220 | /* 221 | * Returns the given authorization object 222 | * */ 223 | getAuthorization(name) { 224 | return this[authorizations][name] || null; 225 | } 226 | 227 | /* 228 | * Returns the given action 229 | * */ 230 | getAction(name) { 231 | return this[actions][name] && this[actions][name].action || null; 232 | } 233 | 234 | /* 235 | * This will perform sanitization on the given input of an intent. 236 | * The sanitize() function returns an object with: 237 | * - default(value:any) -> any default value that can be applied, if the validation fails 238 | * - error (error) -> specific error when the validation fails. 239 | * NOTE: 240 | * it is default() OR error(), not both. 241 | * */ 242 | validate(sanitizerType, opt) { 243 | return new IntentValidator(sanitizerType, opt); 244 | } 245 | 246 | /*------------ INTENT SPECIFIC FUNCTIONS, called by transports or actions. ---------------*/ 247 | 248 | /* 249 | * Start the dispatcher and bind all the actions that were added. 250 | * */ 251 | start() { 252 | if (this.started) return; 253 | // For all our transports, we have to attach all the actions. 254 | this[transports].forEach((tObj) => { 255 | attachActionsToTransport.call(this, tObj); 256 | }); 257 | this.started = true; 258 | } 259 | 260 | /* 261 | * Manually dispatch an action internally, having the AUTHORIZATION 262 | * set to LOCAL 263 | * Arguments: 264 | * - action - the action to execute 265 | * - payload - the raw payload to use. 266 | * - intentObj - if set, we will clone the data from it. 267 | * */ 268 | dispatch(action, payload, _intentObj, _preserveAuth) { 269 | if (typeof payload !== 'object' || !payload) payload = {}; 270 | return new Promise((resolve, reject) => { 271 | let intentObj = new thorin.Intent(action, payload, (wasErr, data) => { 272 | if (wasErr) return reject(data); 273 | resolve(data); 274 | }); 275 | intentObj.transport = 'local'; 276 | if (_intentObj) { 277 | intentObj.client(_intentObj.client()); 278 | let _data = _intentObj.data(); 279 | if(typeof _data === 'object' && _data) { 280 | Object.keys(_data).forEach((d) => { 281 | intentObj.data(d, _data[d]); 282 | }); 283 | } 284 | } 285 | if (_intentObj && _preserveAuth === true && _intentObj.authorizationSource) { 286 | intentObj._setAuthorization(_intentObj.authorizationSource, _intentObj.authorization); 287 | } else { 288 | intentObj._setAuthorization('LOCAL', "NONE"); 289 | } 290 | thorin.dispatcher.triggerIntent(intentObj); 291 | }); 292 | } 293 | 294 | 295 | /* 296 | * This function is called by the transport layer to signal a new intent. 297 | * */ 298 | triggerIntent(intentObj) { 299 | const actionType = intentObj.action, 300 | actionObj = this[actions][actionType] && this[actions][actionType].action || null; 301 | if (!actionObj || actionObj.isTemplate) { // this shouldn't happen. 302 | return intentObj.error(thorin.error('SERVER.NOT_FOUND', 'The requested resource was not found or is currently unavailable.', 404)); 303 | } 304 | 305 | function onCreated(e) { 306 | if (e) { 307 | return intentObj.error(e).send(); 308 | } 309 | actionObj._runStack(intentObj, (e) => { 310 | if (e instanceof Error) { 311 | if (e.name && e.name.indexOf('Thorin') === -1) { 312 | e = thorin.error(e); 313 | } 314 | if (!intentObj.hasError()) { 315 | intentObj.error(e); 316 | } 317 | } 318 | if (!intentObj.completed) { 319 | intentObj.send(); 320 | } 321 | this.emit('intent', intentObj); 322 | }); 323 | } 324 | 325 | onCreated = onCreated.bind(this); 326 | try { 327 | intentObj.runCreate(onCreated); 328 | } catch (e) { 329 | console.error('Thorin.dispatcher.triggerIntent: intent for action ' + actionType + ' threw an error in its runCreate()'); 330 | console.trace(e); 331 | return onCreated(null); 332 | } 333 | } 334 | 335 | /* 336 | * Validates the incoming intent data with the value. 337 | * */ 338 | validateIntent(intentObj, validations, onDone) { 339 | if (typeof validations !== 'object' || validations == null) { 340 | console.error('Thorin.dispatcher.validateIntent: validation data must be a key-value object'); 341 | return onDone(); 342 | } 343 | let calls = [], 344 | inputData = intentObj.rawInput; 345 | Object.keys(validations).forEach((keyName) => { 346 | let validatorObj = validations[keyName]; 347 | if (!(validatorObj instanceof IntentValidator)) { 348 | console.error('Thorin.dispatcher.validateIntent: please use dispatcher.validate() with your action input() field ' + keyName + ' in action ' + intentObj.action); 349 | return; 350 | } 351 | calls.push((done) => { 352 | validatorObj.run(keyName, inputData[keyName], (e, keyValue) => { 353 | if (e) { 354 | e.data = { 355 | field: keyName 356 | }; 357 | return done(e); 358 | } 359 | intentObj.input(keyName, keyValue); 360 | done(); 361 | }); 362 | }); 363 | }); 364 | async.series(calls, onDone); 365 | } 366 | 367 | /* 368 | * Validates the incoming intent's FILTER data. 369 | * */ 370 | validateIntentFilter(intentObj, validations, onDone) { 371 | if (typeof validations !== 'object' || validations == null) { 372 | console.error('Thorin.dispatcher.validateIntentFilter: validation data must be a key-value object'); 373 | return onDone(); 374 | } 375 | let calls = [], 376 | inputData = intentObj.rawFilter || {}; 377 | Object.keys(validations).forEach((keyName) => { 378 | let validatorObj = validations[keyName]; 379 | if (!(validatorObj instanceof IntentValidator)) { 380 | console.error('Thorin.dispatcher.validateIntentFilter: please use dispatcher.validate() with your action input() field ' + keyName + ' in action ' + intentObj.action); 381 | return; 382 | } 383 | calls.push((done) => { 384 | validatorObj.run(keyName, inputData[keyName], (e, keyValue) => { 385 | if (e) { 386 | e.data = { 387 | field: keyName 388 | }; 389 | return done(e); 390 | } 391 | intentObj.filter(keyName, keyValue); 392 | done(); 393 | }); 394 | }); 395 | }); 396 | async.series(calls, onDone); 397 | } 398 | 399 | } 400 | 401 | function checkActionTemplates(actionObj, done) { 402 | let checked = 0, 403 | self = this; 404 | 405 | function checkEmit() { 406 | if (checked !== actionObj.templates.length) return; 407 | if (actionObj.isTemplate) { 408 | self[templates][actionObj.name] = actionObj; 409 | eventerObj.emit('template.' + actionObj.name, actionObj); 410 | } 411 | done && done(); 412 | } 413 | 414 | for (let i = 0; i < actionObj.templates.length; i++) { 415 | let tName = actionObj.templates[i]; 416 | if (typeof this[templates][tName] === 'undefined') { 417 | eventerObj.on('template.' + tName, (templateObj) => { 418 | checked++; 419 | extendAction.call(this, actionObj, templateObj); 420 | checkEmit(); 421 | }); 422 | } else { 423 | checked++; 424 | extendAction.call(this, actionObj, this[templates][tName]); 425 | } 426 | } 427 | checkEmit(); 428 | } 429 | 430 | /* 431 | * Attaches the .template() function to actions 432 | * */ 433 | function extendAction(actionObj, parentActionObj) { 434 | actionObj._extendFromParent(parentActionObj); 435 | } 436 | 437 | /* 438 | * Attach all current actions to a transport. 439 | * */ 440 | function attachActionsToTransport(transportObj, _actions) { 441 | let actionList = (typeof _actions === 'object' ? _actions : this[actions]); 442 | Object.keys(actionList).forEach((name) => { 443 | let actionObj = this[actions][name] && this[actions][name].action; 444 | if (actionObj) { 445 | let opt = this[actions][name].opt; 446 | if (opt.transport && opt.transport !== transportObj.name) return; 447 | } 448 | let k = transportObj._id + ':' + actionObj._id; 449 | if (typeof this[tracking][k] !== 'undefined') return; 450 | transportObj.routeAction(actionObj); 451 | this[tracking][k] = true; 452 | }); 453 | for (let i = 0; i < unsavedActions.length; i++) { 454 | let item = unsavedActions[i]; 455 | if (item.opt.transport && item.opt.transport !== transportObj.name) continue; 456 | let k = transportObj._id + ':' + item.action._id; 457 | if (typeof this[tracking][k] !== 'undefined') continue; 458 | transportObj.routeAction(item.action); 459 | this[tracking][k] = true; 460 | } 461 | } 462 | 463 | return new ThorinDispatcher(); 464 | }; 465 | -------------------------------------------------------------------------------- /lib/routing/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Created by Adrian on 03-Apr-16. 4 | * 5 | * This is a thorin Middleware class. It gives a sense of organisation for 6 | * code. 7 | */ 8 | const async = require('async'); 9 | module.exports = function (thorin) { 10 | 11 | const validate = Symbol(), 12 | use = Symbol(), 13 | filter = Symbol(), 14 | end = Symbol(); 15 | 16 | class ThorinMiddleware { 17 | 18 | constructor(name) { 19 | this.name = name; 20 | this[validate] = []; // array of validations to the intent input 21 | this[filter] = []; 22 | this[end] = []; // array of fns to run 23 | this[use] = []; // an array of functions/middlewares to use. 24 | } 25 | 26 | get stack() { 27 | return this[use]; 28 | } 29 | 30 | get validate() { 31 | return this[validate]; 32 | } 33 | 34 | get _filter() { 35 | return this[filter]; 36 | } 37 | 38 | /* 39 | * Register an intent input validator. 40 | * A validator item must be an object, and its keys are the intent's input keys. 41 | * */ 42 | input(item) { 43 | if (typeof item === 'object' && item) { 44 | this[validate].push(item); 45 | } 46 | return this; 47 | } 48 | 49 | filter(item) { 50 | if (typeof item === 'object' && item) { 51 | this[filter].push(item); 52 | } 53 | return this; 54 | } 55 | 56 | /* 57 | * Middlewares can use other middlewares. 58 | * Or, they can have as many callback functions as they want, 59 | * - OPTIONAL: if a conditionFn is specified, the middleware will be executed only when the result of the conditional function is true. 60 | * Usage: 61 | * .use("otherMiddlewareName", {options}) 62 | * OR 63 | * .use("otherMiddlewareName") 64 | * OR 65 | * .use(function(intentObj, next, opt) {}) 66 | * */ 67 | use(name, opt, conditionFn) { 68 | if (typeof name === 'string') { 69 | if (typeof opt !== 'object') opt = {}; 70 | let item = { 71 | opt: opt, 72 | name: name 73 | }; 74 | if (typeof conditionFn === 'function') item.condition = conditionFn; 75 | this[use].push(item); 76 | } else if (typeof name === 'function') { 77 | let item = { 78 | fn: name, 79 | opt: opt || {} 80 | }; 81 | if (typeof conditionFn === 'function') item.condition = conditionFn; 82 | this[use].push(item); 83 | } 84 | return this; 85 | } 86 | 87 | /* Registers a middleware to be run after intent has completed. */ 88 | end(fn) { 89 | this[end].push(fn); 90 | return this; 91 | } 92 | 93 | /* 94 | * Runs all the functionality that was previously registered with the given intentObj. 95 | * */ 96 | _runStack(intentObj, opt, onDone) { 97 | let calls = []; 98 | /* step one, for each validation, we have to include it in the calls. */ 99 | this[filter].forEach((item) => { 100 | calls.push((done) => { 101 | if (intentObj.completed) return done(); 102 | thorin.dispatcher.validateIntentFilter(intentObj, item, done); 103 | }); 104 | }); 105 | this[validate].forEach((item) => { 106 | calls.push((done) => { 107 | if (intentObj.completed) return done(); 108 | thorin.dispatcher.validateIntent(intentObj, item, done); 109 | }); 110 | }); 111 | 112 | /* Step two: for each use() we try to call it. */ 113 | this[use].forEach((item) => { 114 | if (item.name) { // we have an external middleware. 115 | let middlewareObj = thorin.dispatcher.getMiddleware(item.name); 116 | if (!middlewareObj) { 117 | console.error('Thorin.middleware.runStack: dispatcher does not have a middleware called ' + item.name + ' for middleware ' + this.name); 118 | return; 119 | } 120 | calls.push((done) => { 121 | if (intentObj.completed) return done(); 122 | /* CHECK if we should run the middleware (if it has a condition) */ 123 | if (typeof item.condition === 'function') { 124 | let shouldRun; 125 | try { 126 | shouldRun = item.condition(intentObj); 127 | } catch (e) { 128 | console.error('Thorin.middleware._runStack: use(' + item.fn.name + ') function threw an error in middleware condition for ' + this.name); 129 | return done(); 130 | } 131 | if (shouldRun !== true) return done(); 132 | } 133 | middlewareObj._runStack(intentObj, cloneOpt(item.opt), done); 134 | }); 135 | return; 136 | } 137 | // We have a normal fn. 138 | if (item.fn) { 139 | calls.push((done) => { 140 | if (intentObj.completed) return done(); 141 | try { 142 | item.fn(intentObj, done, cloneOpt(opt)); 143 | } catch (e) { 144 | console.error('Thorin.middleware.runStack: use(' + item.fn.name + ') function threw an error in middleware ' + this.name); 145 | done(thorin.error(e)); 146 | } 147 | }); 148 | } 149 | }); 150 | async.series(calls, (e) => { 151 | calls = null; 152 | intentObj.on('end', () => { 153 | for (let i = 0; i < this[end].length; i++) { 154 | try { 155 | this[end][i](intentObj); 156 | } catch (e) { 157 | console.error('Thorin.middleware: end() callback threw an error in middleware ' + this.name, this[end][i]); 158 | console.error(e); 159 | } 160 | } 161 | }); 162 | onDone(e); 163 | }); 164 | } 165 | 166 | } 167 | 168 | function cloneOpt(opt) { 169 | try { 170 | let keys = Object.keys(opt); 171 | for (let i = 0; i < keys.length; i++) { 172 | let k = keys[i]; 173 | if (typeof opt[k] === 'function') { 174 | return opt; 175 | } 176 | } 177 | return JSON.parse(JSON.stringify(opt)); 178 | } catch (e) { 179 | return opt; 180 | } 181 | } 182 | 183 | return ThorinMiddleware; 184 | }; 185 | -------------------------------------------------------------------------------- /lib/routing/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const async = require('async'); 3 | /** 4 | * Created by Adrian on 06-Apr-16. 5 | * This is the class that is used to validate incoming intent data. 6 | * This is basically a wrapper over the thorin.sanitize() function, that 7 | * also allows async sanitization. 8 | */ 9 | module.exports = function(thorin) { 10 | 11 | const defaultError = Symbol(), 12 | defaultValue = Symbol(), 13 | fieldName = Symbol(), 14 | promises = Symbol(), 15 | options = Symbol(), 16 | callbacks = Symbol(); 17 | 18 | class ThorinIntentValidator { 19 | 20 | constructor(sanitizeType, _opt) { 21 | this.type = sanitizeType; 22 | this[defaultError] = null; 23 | this[defaultValue] = undefined; 24 | this[options] = _opt; // the thorin.sanitize() options 25 | this[promises] = []; // An array of promises that can be attached to the validator. 26 | this[callbacks] = []; // An array of callbacks that can be attached to the validator. 27 | } 28 | 29 | /* Virtual property fieldName */ 30 | set fieldName(v) { 31 | this[fieldName] = v; 32 | } 33 | get fieldName() { 34 | return this[fieldName] || null; 35 | } 36 | 37 | get defaultValue() { 38 | if(typeof this[defaultValue] === 'undefined') return undefined; 39 | if(typeof this[defaultValue] === 'object' && this[defaultValue]) { 40 | // make a copy of it. 41 | return JSON.parse(JSON.stringify(this[defaultValue])); 42 | } 43 | return this[defaultValue]; 44 | } 45 | 46 | /* 47 | * Sets the options that we want to pass to thorin.sanitize(type, input, options) 48 | * */ 49 | options(opt) { 50 | if(typeof opt === 'undefined') return this[options]; 51 | this[options] = opt; 52 | return this; 53 | } 54 | 55 | /* 56 | * Attaches a default value to the intent validator. 57 | * The default value is used when the validation fails. 58 | * */ 59 | default(val) { 60 | if(typeof val === 'undefined') return this[defaultValue]; 61 | if(typeof val !== 'undefined') { 62 | this[defaultValue] = val; 63 | } 64 | return this; 65 | } 66 | 67 | /* 68 | * Attaches a default error to the intent validator. 69 | * The default error is used when the validation fails. 70 | * NOTE: it is either default() or error(), but not both. 71 | * */ 72 | error(err, a, b,c,d) { 73 | if(typeof err === 'undefined') return this[defaultError]; 74 | if(typeof err === 'string') { 75 | err = thorin.error(err,a,b,c,d); 76 | } 77 | if(typeof err !== 'undefined') { 78 | this[defaultError] = err; 79 | } 80 | if(err.ns === 'GLOBAL') err.ns = 'INPUT'; 81 | return this; 82 | } 83 | 84 | /* 85 | * Attach a promise to the validator. 86 | * When a promise is attached, right after the sanitizer completes and contains an 87 | * accepted value, we will run the promises with the resulting value. 88 | * Should the promise fail, we stop the validator. The end result 89 | * of the validator will be the resolved value of the promise. 90 | * EX: 91 | * action.input({ 92 | * firstName: dispatcher.validate("STRING").default("John").promise((inputStr) => {Promise.resolve("John Doe"}) 93 | * }) 94 | * */ 95 | promise(fn) { 96 | if(typeof fn !== 'function') { 97 | console.error('Thorin.sanitize.validate: promise() requires a function as the first argument for validator type ' + this.type); 98 | return this; 99 | } 100 | this[promises].push(fn); 101 | return this; 102 | } 103 | 104 | /* 105 | * Attach a callback to the validator. 106 | * This works exactly as a promise, but in stead doing the then().catch(), it will pass 107 | * an onDone callback function to it. 108 | * */ 109 | callback (fn) { 110 | if(typeof fn !== 'function') { 111 | console.error('Thorin.sanitize.validate: callback() requires a function as the first argument for validator type ' + this.type); 112 | return this; 113 | } 114 | this[callbacks].push(fn); 115 | return this; 116 | } 117 | 118 | /* 119 | * Returns the default failed validation error. 120 | * */ 121 | getFailedError(inputKey) { 122 | return thorin.error('INPUT.NOT_VALID', 'Invalid value for ' + inputKey, 400) 123 | } 124 | 125 | /* 126 | * This is called when we want to apply all the validations. 127 | * Note: this is an asynchronous operation that will require the onDone() function to be a function. 128 | * */ 129 | run(inputKey, inputValue, onDone) { 130 | thorinSanitize.call(this, inputValue, (e, result) => { 131 | if(e) return handleError.call(this, e, onDone); 132 | if(typeof result === 'undefined' || result == null) { 133 | // If we have a default value, use it in stead. 134 | if(typeof this.defaultValue !== 'undefined') { 135 | // IF our default value is null, result with it. 136 | if(this.defaultValue == null) { 137 | return onDone(null, null); 138 | } 139 | result = this.defaultValue; 140 | } else { 141 | // If not, use the default error or the generic one. 142 | if(this[defaultError]) return onDone(this[defaultError]); 143 | return onDone(this.getFailedError(inputKey)); 144 | } 145 | } 146 | if(this[promises].length === 0 && this[callbacks].length === 0) { 147 | return onDone(null, result); 148 | } 149 | // process the results. 150 | // first, go with promises 151 | let calls = []; 152 | this[promises].forEach((promiseFn) => { 153 | calls.push((done) => { 154 | let pObj = promiseFn(result); 155 | if(typeof pObj !== 'object' || !pObj || !pObj.then) { 156 | console.error('Thorin.sanitize.validate: promise callback does not return a promise for validator type ' + this.type + ' in key ' + inputKey); 157 | return done(); 158 | } 159 | let isDone = false; 160 | pObj.then((newResult) => { 161 | if(isDone) return; isDone = true; 162 | result = newResult; 163 | done(); 164 | }, (e) => { 165 | if(isDone) return; isDone = true; 166 | done(e); 167 | }).catch((e) => { 168 | if(isDone) return; isDone = true; 169 | done(e); 170 | }); 171 | }); 172 | }); 173 | // continue with callbacks. 174 | if(this[callbacks].length !== 0) { 175 | this[callbacks].forEach((fn) => { 176 | calls.push((done) => { 177 | fn(result, (e, newResult) => { 178 | if(e) return done(e); 179 | result = newResult; 180 | done(); 181 | }); 182 | }) 183 | }); 184 | } 185 | async.series(calls, (e) => { 186 | if(e) return onDone(e); 187 | onDone(null, result); 188 | }); 189 | }); 190 | } 191 | } 192 | 193 | /* Handles an error. */ 194 | function handleError(e, done) { 195 | if(this.defaultValue) return done(null, this.defaultValue); 196 | if(this[defaultError]) return done(this[defaultError]); 197 | done(e); 198 | } 199 | 200 | /* Calls the thorin sanitizer and checks if it's async or sync */ 201 | function thorinSanitize(input, done) { 202 | let sanitizeRes = thorin.sanitize(this.type, input, this[options]); 203 | if(typeof sanitizeRes === 'object' && sanitizeRes != null && typeof sanitizeRes.then === 'function') { 204 | let isDone = false; 205 | sanitizeRes.then((result) => { 206 | if(isDone) return; isDone = true; 207 | done(null, result); 208 | }, (e) => { 209 | if(isDone) return; isDone = true; 210 | done(e); 211 | }).catch((e) => { 212 | if(isDone) return; isDone = true; 213 | done(e); 214 | }); 215 | return; 216 | } 217 | // otherwise, we have a synchronous value. 218 | done(null, sanitizeRes); 219 | } 220 | 221 | return ThorinIntentValidator; 222 | 223 | }; -------------------------------------------------------------------------------- /lib/util/attached.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Created by Adrian on 20-Mar-16. 4 | * These are attached utilities on the thorin root app. 5 | */ 6 | const async = require('async'), 7 | path = require('path'); 8 | const ThorinError = require('./errors'), 9 | FetchInit = require('./fetch'); 10 | 11 | module.exports = function AttachUtilities(app) { 12 | 13 | /* 14 | * This is an error constructor. It will basically create an error, 15 | * with an errorCode, message and additional options. 16 | * Ways to call: 17 | * thorin.error(code=string) 18 | * thorin.error(code=string, message=string) 19 | * thorin.error(code=string, message=string, statusCode=number) 20 | * thorin.error(code=string, message=string, errorInstance=error) 21 | * thorin.error(errorInstance) -> this will not expose any error messages. 22 | * */ 23 | function thorinErrorWrapper(a, b, c, d) { 24 | if (a instanceof ThorinError.generic) return a; 25 | if (a instanceof Error) { 26 | let e = new ThorinError.generic(a.code || 'GENERIC_ERROR'); 27 | e.source = a; 28 | if (a.statusCode) { 29 | e.statusCode = a.statusCode; 30 | if (a.message) { 31 | e.message = a.message; 32 | } 33 | } else { 34 | e.statusCode = 500; 35 | } 36 | if (typeof a.data !== 'undefined') { 37 | e.data = a.data; 38 | } 39 | // Check if we have a Sequelize error. 40 | return e; 41 | } 42 | if (typeof a === 'string' && !b && !c) { // code 43 | return new ThorinError.generic(a); 44 | } 45 | if (typeof a === 'string' && typeof b === 'string' && !c) { //code,message 46 | let e = new ThorinError.generic(a, b); 47 | if (typeof d === 'number') { 48 | e.statusCode = d; 49 | } 50 | return e; 51 | } 52 | if (typeof a === 'string' && typeof b === 'string' && typeof c === 'number') { // code,message,statusCode 53 | let e = new ThorinError.generic(a, b); 54 | e.statusCode = c; 55 | if (typeof d === 'object' && d != null) { 56 | e.data = d; 57 | } 58 | return e; 59 | } 60 | if (typeof a === 'string' && typeof b === 'string') { 61 | let e = new ThorinError.generic(a, b); 62 | if (c instanceof Error) { 63 | e.source = c; 64 | } else if (typeof c === 'object' && c != null) { 65 | e.data = c; 66 | } 67 | if (typeof d === 'number') { 68 | e.statusCode = d; 69 | } 70 | return e; 71 | } 72 | return new ThorinError.generic(); 73 | } 74 | 75 | app.error = function thorinError() { 76 | let e = thorinErrorWrapper.apply(this, arguments); 77 | return app.parseError(e); 78 | }; 79 | 80 | /* 81 | * Performs a series call through the array of items. 82 | * The items can contain: 83 | * a. functions that return promises, in which case we wait for their resolving. 84 | * b. undefined, in which case we just call and forget. 85 | * Ways to call: 86 | * thorin.series(items=[], stopOnError=false) - we will call all the items, regardless if they fail or not. By default, we stop on errors. 87 | * thorin.series(items=[], onComplete=function(), stopOnError=false) -> will not return a promise, but work with callbacks. 88 | * if you call thorin.series([arrayOfItems], true) 89 | * */ 90 | app.series = function PromiseSeries(items, _onComplete, _stopOnError) { 91 | if (!(items instanceof Array)) throw new Error('thorin.series: requires an array as the first argument.'); 92 | let onComplete = (typeof _onComplete === 'function' ? _onComplete : false), 93 | stopOnError = (_onComplete === false ? false : (_stopOnError !== false)); 94 | if (onComplete) { 95 | return doSeries(items, stopOnError, (e, r) => { 96 | try { 97 | onComplete(e, r); 98 | } catch (err) { 99 | console.error(`Thorin.series() encountered an error in final callback`); 100 | console.trace(err); 101 | } 102 | }); 103 | } 104 | return new Promise((resolve, reject) => { 105 | doSeries(items, stopOnError, (e) => { 106 | if (e) { 107 | reject(e); 108 | return null; 109 | } 110 | resolve(); 111 | return null; 112 | }); 113 | }); 114 | }; 115 | 116 | function doSeries(items, stopOnError, finalFn) { 117 | if (items.length === 0) return finalFn(); 118 | let calls = [], 119 | isStopped = false, 120 | currentNext, 121 | stopError; 122 | 123 | function stopSeries(e) { 124 | isStopped = true; 125 | if (typeof e !== 'undefined') { 126 | stopError = app.error(e); 127 | } 128 | if (currentNext) { 129 | currentNext(); 130 | } 131 | return null; 132 | } 133 | 134 | items.forEach((fn) => { 135 | if (typeof fn !== 'function') return; 136 | calls.push((done) => { 137 | if (isStopped) return done(); 138 | let promiseObj; 139 | currentNext = done; 140 | try { 141 | promiseObj = fn(stopSeries); 142 | } catch (e) { 143 | if (stopOnError) { 144 | return done(e); 145 | } 146 | return done(); 147 | } 148 | let isDone = false; 149 | if (typeof promiseObj === 'object' && promiseObj && typeof promiseObj.then === 'function' && typeof promiseObj.catch === 'function') { 150 | promiseObj.then((res) => { 151 | if (isDone || isStopped) return null; 152 | isDone = true; 153 | done(null, res); 154 | return null; 155 | }, (e) => { 156 | if (isDone || isStopped) return null; 157 | isDone = true; 158 | if (stopOnError) { 159 | isStopped = true; 160 | if (typeof e !== 'undefined') { 161 | stopError = app.error(e); 162 | } 163 | done(e); 164 | return null; 165 | } 166 | done(); 167 | return null; 168 | }); 169 | promiseObj.catch((e) => { 170 | if (isDone || isStopped) return null; 171 | isDone = true; 172 | if (stopOnError) { 173 | isStopped = true; 174 | if (typeof e !== 'undefined') { 175 | stopError = app.error(e); 176 | } 177 | done(e); 178 | return null; 179 | } 180 | done(); 181 | return null; 182 | }); 183 | return null; 184 | } else { 185 | if (isDone || isStopped) return; 186 | isDone = true; 187 | done(); 188 | return null; 189 | } 190 | }); 191 | }); 192 | async.series(calls, (e) => { 193 | if (isStopped) { 194 | if (stopError) { 195 | finalFn(stopError); 196 | return null; 197 | } 198 | finalFn(); 199 | return null; 200 | } 201 | if (e) { 202 | finalFn(e); 203 | return null; 204 | } 205 | finalFn(); 206 | return null; 207 | }); 208 | } 209 | 210 | /* 211 | * Exposes a thorin.fetcher() function that performs thorin-based /dispatch actions to external sources. 212 | * The main difference between this and thorin.fetch() is that this will perform ONLY POST requests 213 | * to a single endpoint (the one given) with a payload of {type, payload} 214 | * */ 215 | const fetchObj = FetchInit(app); 216 | app.fetcher = fetchObj.fetcher; 217 | 218 | /* 219 | * Utility function that uses thorin.root + path for requiring. 220 | * */ 221 | app.require = function RequireFromRoot(_path) { 222 | if (typeof _path !== 'string') throw new Error('thorin.require(path) requires a string path'); 223 | let tmp = path.normalize(thorin.root + '/' + _path); 224 | return require(tmp); 225 | }; 226 | 227 | /* 228 | * Exposes a thorin.fetch() which is a wrapper over the node-fetch module. 229 | * */ 230 | app.fetch = fetchObj.fetch; 231 | app.Error = ThorinError.generic; 232 | }; 233 | -------------------------------------------------------------------------------- /lib/util/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Created by Adrian on 20-Mar-16. 4 | */ 5 | 6 | /* This is a ThorinError that extends the default Error */ 7 | class ThorinGenericError extends Error { 8 | 9 | constructor(_code, _message) { 10 | super(); 11 | this.code = (_code || "GENERIC_ERROR"); 12 | this.message = (_message || "An error occurred."); 13 | this.statusCode = 400; 14 | if (typeof this.code === 'string') { 15 | this.ns = (this.code.indexOf('.') === -1 ? 'GLOBAL' : this.code.split('.')[0]); 16 | } else { 17 | this.ns = 'GENERIC_ERROR'; 18 | } 19 | // this.data -> the data attached. 20 | // this.source -> the parent error. 21 | } 22 | 23 | get name() { 24 | return 'ThorinGenericError' 25 | } 26 | 27 | get stack() { 28 | return this.getStack(); 29 | } 30 | 31 | getStack() { 32 | if (!this.source) return this.stack; 33 | return this.source.stack; 34 | } 35 | 36 | toJSON() { 37 | let d = { 38 | code: this.code, 39 | ns: this.ns, 40 | message: this.message 41 | }; 42 | if (this.data) { 43 | d['data'] = this.data; 44 | } 45 | try { 46 | if (this.source && this.source.fields) { 47 | d.fields = this.source.fields; 48 | } else if(this.fields) { 49 | d.fields = this.fields; 50 | } 51 | } catch (e) { 52 | } 53 | if (this.statusCode) { 54 | d.status = this.statusCode; 55 | } 56 | return d; 57 | } 58 | } 59 | 60 | /* 61 | * Prepares to get attached to thorin.error 62 | * */ 63 | 64 | 65 | module.exports = { 66 | generic: ThorinGenericError 67 | }; 68 | -------------------------------------------------------------------------------- /lib/util/event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter; 3 | /* 4 | * This is simply a wrapper over the EventEmitter that 5 | * */ 6 | module.exports = class Event extends EventEmitter { 7 | constructor() { 8 | super(); 9 | } 10 | 11 | destroy() { 12 | for(let ev in this._events) { 13 | this.removeAllListeners(ev); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /lib/util/fetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const nodeFetch = require('node-fetch'); 3 | /** 4 | * Created by Adrian on 15-Apr-16. 5 | * We can define a set of fetcher sources, which we use to 6 | * fetch data. 7 | */ 8 | module.exports = function (thorin) { 9 | 10 | const fetchers = {}; 11 | let thorinVersion = '1.x'; 12 | try { 13 | thorinVersion = thorin.version.split('.'); 14 | thorinVersion.pop(); //remove latest . 15 | thorinVersion = thorinVersion.join('.'); 16 | } catch (e) { 17 | } 18 | 19 | /* 20 | * Creates a new fetcher instance. 21 | * */ 22 | function createFetcher(url, opt, _name) { 23 | const headers = { 24 | 'Content-Type': 'application/json', 25 | 'User-Agent': 'thorin/' + thorinVersion 26 | }; 27 | if (typeof opt.authorization === 'string') { 28 | let authToken = opt.authorization; 29 | delete opt.authorization; 30 | headers['Authorization'] = 'Bearer ' + authToken; 31 | } 32 | opt = thorin.util.extend({ 33 | method: 'POST', 34 | follow: 1, 35 | timeout: 40000, 36 | headers: headers 37 | }, opt); 38 | 39 | function fetcher(action, _payload) { 40 | return fetcher.dispatch.apply(this, arguments); 41 | } 42 | 43 | /* 44 | * Handles the fetcherObj.dispatch(actionName, _payload) 45 | * NOTE: 46 | * giving a "request" callback in the options will 47 | * call when the http request object is available. 48 | * */ 49 | function doDispatch(action, _payload, _options) { 50 | let bodyPayload = { 51 | type: action, 52 | payload: {} 53 | }; 54 | if (typeof _payload === 'object' && _payload) { 55 | bodyPayload.payload = _payload; 56 | } 57 | let options = (typeof _options === 'object' && _options ? _options : {}); 58 | try { 59 | bodyPayload = JSON.stringify(bodyPayload); 60 | } catch (e) { 61 | return Promise.reject(thorin.error('FETCH.DATA', 'Failed to stringify fetch payload.', e, 400)); 62 | } 63 | let fetchOptions = thorin.util.extend({ 64 | body: bodyPayload, 65 | headers: { 66 | connection: 'keep-alive' 67 | } 68 | }, opt, options); 69 | let statusCode; 70 | return nodeFetch(url, fetchOptions) 71 | .then((res) => { 72 | statusCode = res.status; 73 | return res.json(); 74 | }).then((resultData) => { 75 | if (statusCode >= 200 && statusCode <= 299) { 76 | if (resultData.type) { 77 | delete resultData.type; 78 | } 79 | return Promise.resolve(resultData); 80 | } 81 | const errData = resultData.error || {}, 82 | msg = errData.message || 'Failed to execute fetch', 83 | status = errData.status || 400, 84 | code = (errData.code || 'FETCH.ERROR'); 85 | let err = thorin.error(code, msg, status); 86 | err.ns = 'FETCH'; 87 | if (!err.data) err.data = {}; 88 | err.data.action = action; 89 | throw err; 90 | }).catch((e) => { 91 | if (e && e.ns === 'FETCH') return Promise.reject(e); 92 | let msg = '', 93 | status = 400, 94 | code = 'FETCH.'; 95 | if (e) { 96 | if (e instanceof SyntaxError) { 97 | code += 'RESPONSE'; 98 | msg = 'Request data could not be processed.'; 99 | } else { 100 | switch (e.type) { 101 | case 'request-timeout': 102 | code += 'TIMEOUT'; 103 | msg = 'Request timed out'; 104 | break; 105 | default: 106 | code += 'ERROR'; 107 | msg = 'Could not contact the server'; 108 | status = statusCode || 400; 109 | } 110 | } 111 | } 112 | let tErr = thorin.error(code, msg, status, e); 113 | if (!tErr.data) tErr.data = {}; 114 | tErr.data.action = action; 115 | return Promise.reject(tErr); 116 | }); 117 | } 118 | 119 | fetcher.dispatch = function dispatch(action, _payload) { 120 | let args = Array.prototype.slice.call(arguments); 121 | if (typeof args[args.length - 1] === 'function') { 122 | let fn = args.pop(); 123 | return doDispatch 124 | .apply(this, args) 125 | .then((r) => { 126 | fn(null, r); 127 | }) 128 | .catch((e) => { 129 | fn(e, null); 130 | }); 131 | } 132 | return doDispatch.apply(this, arguments); 133 | }; 134 | 135 | if (_name) { 136 | /* Destroys the fetcher. */ 137 | fetcher.destroy = function DestroyFetcher() { 138 | delete fetchers[_name]; 139 | } 140 | } 141 | 142 | return fetcher; 143 | } 144 | 145 | /* 146 | * thorin.fetcher() will create a Fetch() instance, configured to work 147 | * with the thorin's HTTP transport /dispatch endpoint. Thorin.fetcher 148 | * uses https://www.npmjs.com/package/node-fetch, so any options that it uses 149 | * will be available in opt 150 | * OPTIONS: 151 | * - authorization: the Bearer {TOKEN} authorization header. 152 | * Arguments: 153 | * thorin.fetcher("http://mydomain.com/dispatch", {fetchOptions}) 154 | * thorin.fetcher("myFetcher") => getter of a previous fetcher 155 | * thorin.fetcher("myFetcher", 'http://mydomain.com/dispatch', {}) -> creates a new fetcher, saves & returns it. 156 | * thorin.fetcher("myFetcher", "myAction", {payload}) => returns the fetcher with that name and calls the fetch. 157 | * */ 158 | const fetcherObj = {}; 159 | fetcherObj.fetcher = function fetcher(name, url, opt) { 160 | if (typeof name === 'string' && name.indexOf('://') === -1) { 161 | let fetcherObj = fetchers[name] || null; 162 | if (typeof url === 'undefined') { // thorin.fetcher(name) 163 | return fetcherObj; 164 | } 165 | // thorin.fetcher('name', 'action', {payload}) 166 | if (typeof url === 'string' && fetcherObj) { 167 | return fetcherObj.dispatch(url, opt); 168 | } 169 | } 170 | // thorin.fetcher("https://domain.com/dispatch", {myOpt}) -> create a fetcher that will not be cached. 171 | if (typeof name === 'string' && typeof url === 'object' && url) { 172 | return createFetcher(name, url); 173 | } 174 | // thorin.fetcher("myFetcher", "http://john.com/dispatch", {myOpt}) -> create a fetcher that will be cached. 175 | if (typeof name === 'string' && typeof url === 'string') { 176 | if (typeof opt !== 'object' || !opt) opt = {}; 177 | let fetcherObj = createFetcher(url, opt, name); 178 | if (typeof fetchers[name] !== 'undefined') { 179 | console.error('Thorin.fetcher: fetcher ' + name + ' already cached. Skipping caching.'); 180 | } else { 181 | fetchers[name] = fetcherObj; 182 | } 183 | return fetcherObj; 184 | } 185 | console.warn('Thorin.fetcher: invalid call for fetcher()'); 186 | return thorin; 187 | } 188 | 189 | /* 190 | * This is a wrapper over the node-fetch request-like fetcher. 191 | * ARGUMENTS: 192 | * 193 | * */ 194 | fetcherObj.fetch = function DoFetch(url, opt, done) { 195 | if (typeof url !== 'string') { 196 | console.error('Thorin.fetch() requires the URL as the first argument.'); 197 | return Promise.reject('FETCH.INVALID_URL', 'Invalid or missing URL'); 198 | } 199 | if (typeof opt !== 'object' || !opt) opt = {}; 200 | const headers = { 201 | 'User-Agent': 'thorin/' + thorinVersion 202 | }; 203 | if (typeof opt.authorization === 'string') { 204 | headers['Authorization'] = opt.authorization; 205 | delete opt.authorization; 206 | } 207 | opt = thorin.util.extend({ 208 | follow: 10, 209 | timeout: 40000, 210 | headers: headers 211 | }, opt); 212 | if (typeof opt.body === 'object' && opt.body) { 213 | try { 214 | opt.body = JSON.stringify(opt.body); 215 | if (!opt.headers['Content-Type']) { 216 | opt.headers['Content-Type'] = 'application/json'; 217 | } 218 | } catch (e) { 219 | } 220 | } 221 | let args = Array.prototype.slice.call(arguments); 222 | if (typeof args[args.length - 1] === 'function') { 223 | return doFetch(url, opt, args[args.length - 1]); 224 | } 225 | if (typeof done === 'function') { 226 | return doFetch(url, opt, done); 227 | } 228 | return new Promise((resolve, reject) => { 229 | doFetch(url, opt, (err, res) => { 230 | if (err) return reject(err); 231 | resolve(res); 232 | }); 233 | }); 234 | }; 235 | 236 | function doFetch(url, opt, done) { 237 | let statusCode, 238 | isDone = false; 239 | nodeFetch(url, opt).then((res) => { 240 | statusCode = res.status; 241 | if (statusCode >= 200 && statusCode < 400) { 242 | isDone = true; 243 | return done(null, res); 244 | } 245 | let contentType = res.headers.get('content-type'); 246 | if (contentType && contentType.indexOf('/json') !== -1) { 247 | return res.json().then((err) => { 248 | if (isDone) return; 249 | isDone = true; 250 | let errData = {}; 251 | if (typeof err === 'object' && err) { 252 | if (typeof err.error === 'object' && err.error) { 253 | errData = err.error; 254 | } else { 255 | errData.error = err; 256 | } 257 | } 258 | let msg = errData.message || 'Failed to execute fetch', 259 | status = errData.status || statusCode, 260 | code = errData.code || 'FETCH.ERROR'; 261 | let tErr = thorin.error(code, msg, status, (typeof err === 'object' && err && !err.error ? err : null)); 262 | tErr.ns = errData.ns || 'FETCH'; 263 | if (!tErr.data) tErr.data = {}; 264 | tErr.data.url = url; 265 | if (errData.error) tErr.error = errData.error; 266 | isDone = true; 267 | done(tErr); 268 | }); 269 | } 270 | return res.text().then((text) => { 271 | if (isDone) return; 272 | isDone = true; 273 | let tErr = thorin.error('FETCH.ERROR', 'Could not contact server', statusCode); 274 | tErr.data = text; 275 | isDone = true; 276 | done(tErr); 277 | }); 278 | }).catch((e) => { 279 | if (isDone) return; 280 | isDone = true; 281 | let msg = '', 282 | status = 400, 283 | code = 'FETCH.'; 284 | if (e) { 285 | if (e instanceof SyntaxError) { 286 | code += 'RESPONSE'; 287 | msg = 'Request data could not be processed'; 288 | } else { 289 | switch (e.type) { 290 | case 'request-timeout': 291 | code += 'TIMEOUT'; 292 | msg = 'Request timed out'; 293 | break; 294 | default: 295 | code += 'ERROR'; 296 | msg = 'Could not retrieve server data'; 297 | status = statusCode || 400; 298 | } 299 | } 300 | } 301 | let tErr = thorin.error(code, msg, status, e); 302 | if (!tErr.data) tErr.data = {}; 303 | tErr.data.url = url; 304 | done(tErr); 305 | }); 306 | } 307 | 308 | return fetcherObj; 309 | } 310 | -------------------------------------------------------------------------------- /lib/util/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const crypto = require('crypto'), 3 | dotObject = require('dot-object'), 4 | path = require('path'), 5 | url = require('url'), 6 | uuid = require('uuid'), 7 | http = require('http'), 8 | https = require('https'), 9 | async = require('async'), 10 | nodeFetch = require('node-fetch'), 11 | fs = require('fs'), 12 | fse = require('fs-extra'), 13 | extend = require('extend'), 14 | Event = require('./event.js'); 15 | /** 16 | * Utilities used globally. 17 | */ 18 | var ALPHA_NUMERIC_CHARS = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890", 19 | ALPHA_NUMERIC_SPECIAL = ALPHA_NUMERIC_CHARS + '@#$^&()[]+-.', 20 | RANDOM_STRING = '', 21 | RANDOM_STRING_ALPHA = ''; 22 | 23 | (() => { 24 | for (var i = 0; i <= 255; i++) { 25 | var q = Math.floor(Math.random() * ALPHA_NUMERIC_SPECIAL.length); 26 | RANDOM_STRING += ALPHA_NUMERIC_SPECIAL.charAt(q); 27 | var r = Math.floor(Math.random() * ALPHA_NUMERIC_CHARS.length); 28 | RANDOM_STRING_ALPHA += ALPHA_NUMERIC_CHARS.charAt(r); 29 | } 30 | })(); 31 | 32 | class ThorinUtils { 33 | 34 | /** 35 | * Generate a random number between {a,b} 36 | * This is a cryptographically-safe function that does not use Math.random (lol) 37 | * @param {a} 38 | * @param {b} 39 | * NOTE: 40 | * - If a is given and b is not given, we do random(length) 41 | * - If a AND b is given, we do random(a,b) - number between a and b 42 | * */ 43 | static randomNumber(length, b) { 44 | let res = ''; 45 | if (typeof length === 'number' && typeof b === 'number') { 46 | let a = length; 47 | if (a >= b) throw new Error('A must be smaller than b.'); 48 | let rl = Math.max(a.toString().length, b.toString().length); 49 | let r = crypto.randomBytes(rl), 50 | bigRand = ''; 51 | for (let i = 0, len = rl; i < len; i++) { 52 | bigRand += r.readUInt8(i).toString(); 53 | if (bigRand.length > rl * 2) break; 54 | } 55 | bigRand = parseFloat('0.' + bigRand); 56 | res = Math.floor(bigRand * (a - b + 1) + b); 57 | return res; 58 | } 59 | if (typeof length !== 'number' || !length || length <= 0) length = 6; 60 | let buff = crypto.randomBytes(length); 61 | for (let i = 0; i < length; i++) { 62 | let t = buff.readUInt8(i).toString(), 63 | pos = Math.floor(Math.random() * t.length); 64 | if (pos > 0) pos--; 65 | res += t.substr(pos); 66 | if (res.length >= length) { 67 | res = res.substr(0, length); 68 | break; 69 | } 70 | } 71 | return parseInt(res); 72 | } 73 | 74 | /** 75 | * We generate x random bytes, then we select based on the byte's number, a char from the ALPHA_NUMERIC strings 76 | * @function randomString 77 | * @param {number} length - the length of the string to be generated. 78 | * @param {Function} callback - the callback to call when it's ready. 79 | * */ 80 | static randomString(length, _onlyAlpha) { 81 | if (typeof length !== 'number') length = 16; // random 16 by default. 82 | var gen = Math.abs(parseInt(length)), 83 | onlyAlpha = (_onlyAlpha !== false); 84 | try { 85 | var buf = crypto.randomBytes(gen); 86 | } catch (e) { 87 | console.warn('Thorin.util.randomString: failed to generate crypto random buffer: ', e); 88 | return null; 89 | } 90 | var res = ''; 91 | for (var i = 0; i < gen; i++) { 92 | var _poz = buf.readUInt8(i); 93 | if (onlyAlpha) { 94 | res += RANDOM_STRING_ALPHA.charAt(_poz); 95 | } else { 96 | res += RANDOM_STRING.charAt(_poz); 97 | } 98 | } 99 | return res; 100 | } 101 | 102 | /* 103 | * Wrapper over the deep extend(). Same as Object.extend() but deep copies. 104 | * It should not be called with a target, because the target will be returned. 105 | * */ 106 | static extend(sources) { 107 | let target = {}; 108 | let args = Array.prototype.slice.call(arguments); 109 | args.reverse(); 110 | args.push(target); 111 | args.push(true); 112 | args = args.reverse(); 113 | return extend.apply(extend, args); 114 | } 115 | 116 | /* 117 | * Recursively reads the contents of the given folder path and returns an array with file paths. 118 | * Options: 119 | * opt.ext - the extension to search for, OR 120 | * opt.dirs - if set to true, returns only directories. 121 | * opts.levels - if set, the number of levels to go in. 122 | * opt.modules=false - if set to true, we will go through node_modules. 123 | * opt.relative= false - if set true, we will convert all paths to be relative to the root path, EXCLUDING the first "/" 124 | * */ 125 | static readDirectory(dirPath, opt, __res, __level) { 126 | dirPath = path.normalize(dirPath); 127 | if (typeof opt !== 'object' || !opt) opt = {}; 128 | if (typeof __level !== 'number') __level = 0; 129 | if (typeof __res === 'undefined') __res = []; 130 | let items = [], 131 | ext = null; 132 | try { 133 | items = fs.readdirSync(dirPath); 134 | } catch (e) { 135 | return __res; 136 | } 137 | if (opt.ext) { 138 | ext = opt.ext; 139 | delete opt.dirs; 140 | if (ext && ext.charAt(0) !== '.') ext = '.' + ext; 141 | } 142 | 143 | if (opt.levels && __level >= opt.levels) { 144 | return __res; 145 | } else { 146 | __level++; 147 | } 148 | // sort items with files first. 149 | items = items.sort((a, b) => { 150 | if (a.indexOf('.') === -1) return 1; 151 | return -1; 152 | }); 153 | for (let i = 0; i < items.length; i++) { 154 | let itemPath = path.normalize(dirPath + "/" + items[i]), 155 | item = fs.lstatSync(itemPath); 156 | if (opt.dirs !== true && item.isFile()) { 157 | if (!ext || (ext && path.extname(itemPath) === ext || items[i] === ext)) { 158 | __res.push(itemPath); 159 | } 160 | } else if (item.isDirectory()) { 161 | let shouldGoDeeper = true; 162 | // Check if the dir starts with ".", If so, ignore. 163 | if (items[i].charAt(0) === '.') { 164 | shouldGoDeeper = false; 165 | } else { 166 | if (opt.dirs && items[i] !== 'node_modules') { 167 | __res.push(itemPath); 168 | } 169 | if (items[i] === 'node_modules' && opt.modules !== true) { 170 | shouldGoDeeper = false; 171 | } 172 | } 173 | if (shouldGoDeeper) { 174 | ThorinUtils.readDirectory(itemPath, opt, __res, __level); 175 | } 176 | } 177 | } 178 | if (opt.relative === true && __level === 1) { 179 | for (let i = 0; i < __res.length; i++) { 180 | __res[i] = __res[i].replace(dirPath, ''); 181 | if (__res[i].charAt(0) === path.sep) { 182 | __res[i] = __res[i].substr(1); 183 | } 184 | } 185 | } 186 | return __res; 187 | } 188 | 189 | /* 190 | * Checks if the given path is a file. Simple check with try catch and returns true/false 191 | * */ 192 | static isFile(fpath) { 193 | try { 194 | let stat = fs.lstatSync(path.normalize(fpath)); 195 | if (stat.isFile()) return true; 196 | return false; 197 | } catch (e) { 198 | return false; 199 | } 200 | } 201 | 202 | /* 203 | * Checks if the given path is a directory. 204 | * */ 205 | static isDirectory(dpath) { 206 | try { 207 | let stat = fs.lstatSync(path.normalize(dpath)); 208 | if (stat.isDirectory()) return true; 209 | return false; 210 | } catch (e) { 211 | return false; 212 | } 213 | } 214 | 215 | /** 216 | * Utility function that hashes the given text using SHA1 (128 bits) 217 | * @function sha1 218 | * @param {string} - the string to be hashed 219 | * @returns {string} 220 | * */ 221 | static sha1(text) { 222 | return crypto.createHash('sha1').update(text).digest('hex'); 223 | } 224 | 225 | /** 226 | * Utility function that hashes the given text using SHA2 (256 bits) 227 | * @function sha2 228 | * @param {string} - the string to be hashed 229 | * @param {number=1} - the number of times we want to perform the sha2 230 | * @returns {string} 231 | * */ 232 | static sha2(text, _count) { 233 | var hash = crypto.createHash('sha256').update(text).digest('hex'); 234 | if (typeof _count === 'number' && _count > 1) { 235 | for (var i = 0; i < _count - 1; i++) { 236 | hash = crypto.createHash('sha256').update(hash).digest('hex'); 237 | } 238 | } 239 | return hash; 240 | } 241 | 242 | /** 243 | * Utility function that creates a sha2 HMAC with a secret seed 244 | * */ 245 | static hmac(text, secret, _alg) { 246 | if (!_alg) _alg = 'sha256'; 247 | var hash = crypto.createHmac(_alg, secret) 248 | .update(text) 249 | .digest('hex'); 250 | return hash; 251 | } 252 | 253 | /* 254 | * Safely compares two strings, by performing an XOR on them. 255 | * We use this to mitigate string comparison hack 256 | * */ 257 | static compare(a, b) { 258 | if (typeof a !== 'string' || typeof b !== 'string') return false; 259 | let wrong = 0, 260 | max = Math.max(a.length, b.length); 261 | for (let i = 0; i < max; i++) { 262 | if (a[i] !== b[i]) wrong++; 263 | } 264 | return wrong === 0; 265 | } 266 | 267 | /** 268 | * Synchronously encrypts the given data with the given key, by default WITH NO INITIALIZATION VECTOR. 269 | * If the IV is specified and present, it will be used. 270 | * IF the IV is present, we hex encode it and prepend it to the ciphertext, followed by a $ 271 | * Returns hex-encrypted text or false, if failed. 272 | * */ 273 | static encrypt(data, encryptionKey, _useIv = true) { 274 | try { 275 | let cipher, 276 | iv; 277 | if (_useIv === true) { 278 | iv = crypto.randomBytes(16); 279 | } else if (typeof _useIv === 'string') { 280 | try { 281 | iv = new Buffer(_useIv, 'hex'); 282 | } catch (e) { 283 | } 284 | } else if (typeof _useIv === 'object') { 285 | iv = _useIv; 286 | } 287 | if (encryptionKey.length > 32) { 288 | encryptionKey = encryptionKey.substr(0, 32); 289 | } 290 | let isEncryptionKeyOk = encryptionKey.length >= 32; 291 | if (global.THORIN_DISABLE_IV_KEY) iv = undefined; 292 | if (typeof iv !== 'undefined' && isEncryptionKeyOk) { 293 | cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv); 294 | } else { 295 | cipher = crypto.createCipher('aes-256-cbc', encryptionKey); 296 | iv = undefined; 297 | } 298 | 299 | if (!(data instanceof Buffer) && typeof data !== 'string') { 300 | if (typeof data === 'object' && data != null) { 301 | data = JSON.stringify(data); 302 | } else { 303 | data = data.toString(); 304 | } 305 | } 306 | var encrypted = cipher.update(data, 'utf8', 'hex'); 307 | encrypted += cipher.final('hex'); 308 | if (typeof iv !== 'undefined') { 309 | encrypted = iv.toString('hex') + '$' + encrypted; 310 | } 311 | return encrypted; 312 | } catch (err) { 313 | console.warn('Thorin.util.encrypt: Failed to synchronously encrypt data', err); 314 | return false; 315 | } 316 | } 317 | 318 | /** 319 | * Synchronously tries to decrypt the given data with the given encryption key. By default, 320 | * it will not make use of any IV, but if specified, it will be used. 321 | * Returns the decrypted string, or false, if failed to decrypt. 322 | * */ 323 | static decrypt(data, encryptionKey, _iv) { 324 | if (typeof data !== 'string' || !data || typeof encryptionKey !== 'string' || !encryptionKey) { 325 | return false; 326 | } 327 | try { 328 | let decipher, iv; 329 | if (data.charAt(32) === '$' && typeof _iv === 'undefined') { 330 | iv = data.substr(0, 32); 331 | data = data.substr(33); 332 | try { 333 | iv = new Buffer(iv, 'hex'); 334 | } catch (e) { 335 | } 336 | } else if (typeof _iv !== 'undefined') { 337 | try { 338 | iv = new Buffer(_iv, 'hex'); 339 | } catch (e) { 340 | } 341 | } 342 | if (encryptionKey.length > 32) { 343 | encryptionKey = encryptionKey.substr(0, 32); 344 | } 345 | if (iv) { 346 | decipher = crypto.createDecipheriv('aes-256-cbc', encryptionKey, iv); 347 | } else { 348 | decipher = crypto.createDecipher('aes-256-cbc', encryptionKey); 349 | } 350 | var decoded = decipher.update(data, 'hex', 'utf8'); 351 | decoded += decipher.final('utf8'); 352 | return decoded; 353 | } catch (e) { 354 | return false; 355 | } 356 | }; 357 | 358 | /** 359 | * Downloads a given static css/js resource from the given url and returns the string. 360 | * */ 361 | static downloadFile(urlPath, done) { 362 | let opt; 363 | try { 364 | opt = url.parse(urlPath); 365 | } catch (e) { 366 | return done(e); 367 | } 368 | let hLib = (opt.protocol === 'http:' ? http : https), 369 | downloadOpt = { 370 | hostname: opt.hostname, 371 | port: opt.port, 372 | path: opt.path 373 | }; 374 | hLib.get(downloadOpt, (res) => { 375 | let data = ''; 376 | if (res.statusCode < 200 || res.statusCode > 299) { 377 | res.on('error', () => { 378 | }); 379 | return done(new Error('The requested resource is not available.')); 380 | } 381 | let contentType = res.headers['content-type']; 382 | if (typeof contentType !== 'string' || !contentType) { 383 | return done(new Error("The requested resource type is not supported.")); 384 | } 385 | if (contentType.indexOf('text/') !== 0) { // check for application/javascript, application/xml only. 386 | if (contentType !== 'application/javascript' && contentType !== 'application/xml') { 387 | res.on('error', () => { 388 | }); 389 | return done(new Error("The requested resource type is not supported.")); 390 | } 391 | } 392 | let wasSent = false; 393 | res 394 | .on('data', (d) => { 395 | if (wasSent) return; 396 | data += d; 397 | }) 398 | .on('end', () => { 399 | if (wasSent) return; 400 | wasSent = true; 401 | done(null, data); 402 | }) 403 | .on('error', (e) => { 404 | if (wasSent) return; 405 | wasSent = true; 406 | done(e); 407 | }); 408 | }).on('error', done); 409 | } 410 | 411 | /** 412 | * Given an object and a key composed of multiple dots, it will return the inner key or null. 413 | * Ex: 414 | * setting.notify.id will try and return setting[notify][id] 415 | * */ 416 | static innerKey(obj, key, _val, _forceVal) { 417 | if (typeof obj !== 'object' || !obj) return null; 418 | if (typeof key !== 'string' || key === '') return null; 419 | if (key.indexOf('.') === -1) { 420 | if (typeof _val !== 'undefined' || _forceVal) { 421 | if (_forceVal) { 422 | delete obj[key]; 423 | } else { 424 | obj[key] = _val; 425 | } 426 | } 427 | return obj[key]; 428 | } 429 | var s = key.split('.'); 430 | var tmp = obj; 431 | try { 432 | for (var i = 0; i < s.length; i++) { 433 | tmp = tmp[s[i]]; 434 | if ((typeof _val !== 'undefined' || _forceVal) && i === s.length - 2) { 435 | if (_forceVal) { 436 | delete tmp[s[i + 1]]; 437 | } else { 438 | // this is the inner setter. 439 | tmp[s[i + 1]] = _val; 440 | } 441 | } 442 | } 443 | if (typeof tmp === 'undefined') return null; 444 | return tmp; 445 | } catch (e) { 446 | return null; 447 | } 448 | } 449 | 450 | /** 451 | * Given an object that contains dotted-keys, we will convert 452 | * the dot keys into objects. 453 | * Arguments: 454 | * - obj - the object with keys containing . 455 | * */ 456 | static dotObject(obj, _key) { 457 | if (typeof obj !== 'object' || !obj) return {}; 458 | let src = {}; 459 | try { 460 | src = dotObject.object(obj); 461 | } catch (e) { 462 | if (typeof _key === 'string' && _key) { 463 | return null; 464 | } 465 | return src; 466 | } 467 | if (typeof _key === 'string' && _key && _key.trim() !== '') { 468 | return this.innerKey(obj, _key); 469 | } 470 | return src; 471 | } 472 | 473 | 474 | /** 475 | * Given an object, it will flatten it and return an array with dotted-keys and their values 476 | * */ 477 | static flattenObject(obj) { 478 | if (typeof obj !== 'object' || !obj) { 479 | return []; 480 | } 481 | try { 482 | return dotObject.dot(obj); 483 | } catch (e) { 484 | return []; 485 | } 486 | } 487 | 488 | /** 489 | * Given a number and a max length, it will format the number and retrieve the 0-prefixed string 490 | * EG: 491 | * numberPrefix(5, 999999) => 000005 492 | * */ 493 | static numberPrefix(number, maxNumber, _prefix) { 494 | let parsed = (typeof _prefix === 'string' ? _prefix : ''); 495 | if (typeof number === 'number') number = number.toString(); 496 | if (typeof number !== 'string' || !number) return parsed; 497 | if (typeof maxNumber === 'number' && maxNumber >= 0) { 498 | if (parseInt(number, 10) > maxNumber) return ''; 499 | maxNumber = maxNumber.toString(); 500 | } 501 | if (typeof maxNumber === 'string' && maxNumber) { 502 | for (let i = 0; i < (maxNumber.length - number.length); i++) { 503 | parsed += '0'; 504 | } 505 | } 506 | parsed += number; 507 | return parsed; 508 | } 509 | 510 | /* 511 | * Given an array of items (string/number), returns the unique ones. 512 | * */ 513 | static unique(items, field) { 514 | if (!(items instanceof Array)) return []; 515 | let uMap = {}, 516 | result = []; 517 | if (typeof field !== 'string' || !field) field = 'id'; 518 | for (let i = 0, len = items.length; i < len; i++) { 519 | let itm = items[i]; 520 | if (typeof itm === 'object' && itm) { 521 | itm = itm[field]; 522 | } 523 | if (typeof itm !== 'string' && typeof itm !== 'number') continue; 524 | if (typeof uMap[itm] !== 'undefined') continue; 525 | uMap[itm] = true; 526 | result.push(itm); 527 | } 528 | return result; 529 | } 530 | 531 | /** 532 | * Given an array with objects, a key name and another array with objects, and another key name, it will map them together 533 | * Example: 534 | * source = [{ 535 | * id: 1, 536 | * device_id: '1' 537 | * }]; 538 | * devices = [{ 539 | * id: '1', 540 | * name: 'test' 541 | * }] 542 | * mergeItems(source, 'device', devices, 'id') 543 | * => 544 | * source = [{ 545 | * id: 1, 546 | * device: { 547 | * id: '1', 548 | * name: 'test' 549 | * } 550 | * }] 551 | * */ 552 | static mergeItems(source, sourceField, items, itemField, _separator) { 553 | if (typeof sourceField !== 'string' || !sourceField) return false; 554 | if (typeof source === 'undefined') return false; 555 | if (typeof items === 'undefined') return false; 556 | if (!(source instanceof Array)) source = [source]; 557 | if (!(items instanceof Array)) items = [items]; 558 | if (typeof itemField !== 'string' || !itemField) itemField = 'id'; 559 | if (typeof _separator !== 'string' || !_separator) _separator = '_'; 560 | let targetMap = {}, 561 | sourceId = sourceField + _separator + itemField; 562 | // loop over items 563 | for (let i = 0, len = items.length; i < len; i++) { 564 | let itm = items[i]; 565 | if (typeof itm !== 'object' || !itm) continue; 566 | let tid = itm[itemField]; 567 | if (typeof tid === 'number') tid = tid.toString(); 568 | if (typeof tid !== 'string' || !tid) continue; 569 | targetMap[tid] = itm; 570 | } 571 | 572 | // loop over source. 573 | for (let i = 0, len = source.length; i < len; i++) { 574 | let itm = source[i]; 575 | if (typeof itm !== 'object' || !itm) continue; 576 | let sid = itm[sourceId]; 577 | if (typeof sid === 'number') sid = sid.toString(); 578 | if (typeof sid !== 'string' || !sid) continue; 579 | let targetObj = targetMap[sid]; 580 | if (typeof targetObj !== 'object' || !targetObj) continue; 581 | itm[sourceField] = targetObj; 582 | } 583 | // next loop over targets 584 | targetMap = null; 585 | } 586 | 587 | /** 588 | * Similar with mergeItems() but works with arrays in stead of source_id/items mapping 589 | * Example: 590 | * let items = [{ 591 | id: '1', 592 | name: 'Item one' 593 | }, { 594 | id: '2', 595 | name: 'Item two' 596 | }]; 597 | let sourceField = 'profiles'; 598 | let items = [{ 599 | id: 'something', 600 | account_id: '1', 601 | name: 'John' 602 | }, { 603 | id: 'something2', 604 | account_id: '2', 605 | name: 'Doe' 606 | }]; 607 | let itemKeyName = 'account_id'; 608 | let sourceKeyName = 'id'; 609 | let result = [{ 610 | id: '1', 611 | name: 'Item one', 612 | profiles: [{ 613 | id: '1', 614 | account_id: '1', 615 | name: 'John' 616 | }] 617 | }, { 618 | id: '2', 619 | name: 'Item two', 620 | profiles: [{ 621 | id: '2', 622 | account_id: '2', 623 | name: 'Doe' 624 | }] 625 | }]*/ 626 | static mergeArrayItems(sources, sourceField, items, itemKeyName, sourceKeyName) { 627 | if (!(sources instanceof Array)) return false; 628 | if (typeof sourceField !== 'string') return false; 629 | if (!(items instanceof Array)) return false; 630 | if (typeof itemKeyName !== 'string') return false; 631 | if (typeof sourceKeyName !== 'string') sourceKeyName = 'id'; 632 | let sourceMap = {}; 633 | // loop over our items to match and map the source field. 634 | for (let i = 0, len = items.length; i < len; i++) { 635 | let id = items[i][itemKeyName]; 636 | if (typeof id === 'number') id = id.toString(); 637 | if (typeof id !== 'string') continue; 638 | if (typeof sourceMap[id] === 'undefined') sourceMap[id] = []; 639 | sourceMap[id].push(items[i]); 640 | } 641 | // loop over the sources and search through the searchMap to map the items. 642 | for (let i = 0, len = sources.length; i < len; i++) { 643 | let id = sources[i][sourceKeyName]; 644 | if (typeof id === 'number') id = id.toString(); 645 | if (typeof id !== 'string') continue; 646 | if (typeof sourceMap[id] !== 'undefined') { 647 | sources[i][sourceField] = sourceMap[id]; 648 | } else { 649 | sources[i][sourceField] = []; 650 | } 651 | } 652 | sourceMap = null; 653 | return true; 654 | } 655 | 656 | /** 657 | * Similar to mergeItems, it will essentially check if the given array items are objects and contain the field as an object. 658 | * EG: 659 | * items=[{ 660 | * id: '1', 661 | * asset: { 662 | * id: 2 663 | * } 664 | * }, { 665 | * id: 3 666 | * }] 667 | * field=asset 668 | * => items=[{id: '1', asset: {id: 2}}] 669 | * */ 670 | static cleanItems(items, field) { 671 | if (!(items instanceof Array)) items = [items]; 672 | let i = 0; 673 | if (typeof field !== 'string' || !field) return items; 674 | while (i < items.length) { 675 | let item = items[i]; 676 | if (typeof item !== 'object' || !item || typeof item[field] !== 'object' || !item[field]) { 677 | items.splice(i, 1); 678 | } else { 679 | i++; 680 | } 681 | } 682 | return items; 683 | } 684 | 685 | /** 686 | * Utility function that can be used to clean arrays. 687 | * Arguments: 688 | * - items[] -> an array of objects/strings/numbers (not booleans) 689 | * - fn-> a callback function to check every item. 690 | * */ 691 | static mapItems(items, fn) { 692 | let clean = []; 693 | if (!(items instanceof Array)) return clean; 694 | for (let i = 0, len = items.length; i < len; i++) { 695 | let item = items[i]; 696 | if (typeof item === 'undefined' || item == null) continue; 697 | if (item === false || item === true) continue; 698 | try { 699 | let tmp = fn(item); 700 | if (typeof tmp === 'undefined' || tmp == null || tmp === false) continue; 701 | if (tmp === true) { 702 | clean.push(items); 703 | } else { 704 | clean.push(tmp); 705 | } 706 | } catch (e) { 707 | } 708 | } 709 | return clean; 710 | } 711 | 712 | /** 713 | * Utility function that will search for the given needle 714 | * in all the given arguments. 715 | * Eg: 716 | * let match = thorin.util.matchText('hi!', 'john', 'doe', 'will say hi!'); // true 717 | * */ 718 | static matchText(needle) { 719 | let args = Array.prototype.slice.call(arguments); 720 | args.splice(0, 1); 721 | if (typeof needle !== 'string' || !needle) return false; 722 | if (args[0] instanceof Array) args = args[0]; 723 | if (args.length === 0) return false; 724 | needle = needle.toLowerCase().trim(); 725 | let clean = []; 726 | // First, match entire string 727 | for (let i = 0, len = args.length; i < len; i++) { 728 | if (typeof args[i] === 'number') args[i] = args[i].toString(); 729 | if (typeof args[i] !== 'string') continue; 730 | args[i] = args[i].trim().toLowerCase(); 731 | if (args[i].indexOf(needle) !== -1) return true; 732 | clean.push(args[i]); 733 | } 734 | // Next, split needle into words. 735 | if (clean.length === 0) return false; 736 | let words = needle.split(' '); 737 | for (let i = 0; i < words.length; i++) { 738 | let word = words[i].trim(); 739 | if (!word) continue; 740 | for (let j = 0; j < clean.length; j++) { 741 | if (clean[j].indexOf(word) !== -1) return true; 742 | } 743 | } 744 | return false; 745 | } 746 | } 747 | 748 | /** 749 | * Expose the fs-extra library in thorin.util.fs 750 | * */ 751 | ThorinUtils.fs = fse; 752 | /* 753 | * Expose the thorin.util.Event class 754 | * */ 755 | ThorinUtils.Event = Event; 756 | /** 757 | * Expose the async library in thorin.util.async 758 | * */ 759 | ThorinUtils.async = async; 760 | 761 | /** 762 | * Expose the node-fetch library in thorin.util.fetch 763 | * */ 764 | ThorinUtils.fetch = nodeFetch; 765 | 766 | /** 767 | * Expose the uuid library in thorin.util.uuid 768 | * */ 769 | ThorinUtils.uuid = uuid; 770 | ThorinUtils.dot = dotObject; 771 | module.exports = ThorinUtils; 772 | -------------------------------------------------------------------------------- /npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thorin", 3 | "version": "1.5.7", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "thorin", 9 | "version": "1.5.7", 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "3.1.1", 13 | "camelcase": "5.3.1", 14 | "colors": "1.4.0", 15 | "dot-object": "2.1.2", 16 | "extend": "3.0.2", 17 | "fs-extra": "8.1.0", 18 | "node-fetch": "2.6.1", 19 | "promise.prototype.finally": "3.1.2", 20 | "thorin-sanitize": "^1.1.6", 21 | "uuid": "3.4.0" 22 | } 23 | }, 24 | "node_modules/async": { 25 | "version": "3.1.1", 26 | "resolved": "https://registry.npmjs.org/async/-/async-3.1.1.tgz", 27 | "integrity": "sha512-X5Dj8hK1pJNC2Wzo2Rcp9FBVdJMGRR/S7V+lH46s8GVFhtbo5O4Le5GECCF/8PISVdkUA6mMPvgz7qTTD1rf1g==" 28 | }, 29 | "node_modules/balanced-match": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 32 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 33 | }, 34 | "node_modules/brace-expansion": { 35 | "version": "1.1.11", 36 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 37 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 38 | "dependencies": { 39 | "balanced-match": "^1.0.0", 40 | "concat-map": "0.0.1" 41 | } 42 | }, 43 | "node_modules/camelcase": { 44 | "version": "5.3.1", 45 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 46 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 47 | "engines": { 48 | "node": ">=6" 49 | } 50 | }, 51 | "node_modules/colors": { 52 | "version": "1.4.0", 53 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 54 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 55 | "engines": { 56 | "node": ">=0.1.90" 57 | } 58 | }, 59 | "node_modules/commander": { 60 | "version": "4.1.1", 61 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 62 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 63 | "engines": { 64 | "node": ">= 6" 65 | } 66 | }, 67 | "node_modules/concat-map": { 68 | "version": "0.0.1", 69 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 70 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 71 | }, 72 | "node_modules/define-properties": { 73 | "version": "1.1.3", 74 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 75 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 76 | "dependencies": { 77 | "object-keys": "^1.0.12" 78 | }, 79 | "engines": { 80 | "node": ">= 0.4" 81 | } 82 | }, 83 | "node_modules/dot-object": { 84 | "version": "2.1.2", 85 | "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.2.tgz", 86 | "integrity": "sha512-Hx7gYDQfb4umAku7VJW0fr3SXXTaghfBhYNbc4eHTjkgPMoY4DAw210j76mSSlV9LiTozVswT462e4xL7w3GZA==", 87 | "dependencies": { 88 | "commander": "^4.0.0", 89 | "glob": "^7.1.5" 90 | }, 91 | "bin": { 92 | "dot-object": "bin/dot-object" 93 | } 94 | }, 95 | "node_modules/es-abstract": { 96 | "version": "1.17.4", 97 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", 98 | "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", 99 | "dependencies": { 100 | "es-to-primitive": "^1.2.1", 101 | "function-bind": "^1.1.1", 102 | "has": "^1.0.3", 103 | "has-symbols": "^1.0.1", 104 | "is-callable": "^1.1.5", 105 | "is-regex": "^1.0.5", 106 | "object-inspect": "^1.7.0", 107 | "object-keys": "^1.1.1", 108 | "object.assign": "^4.1.0", 109 | "string.prototype.trimleft": "^2.1.1", 110 | "string.prototype.trimright": "^2.1.1" 111 | }, 112 | "engines": { 113 | "node": ">= 0.4" 114 | }, 115 | "funding": { 116 | "url": "https://github.com/sponsors/ljharb" 117 | } 118 | }, 119 | "node_modules/es-to-primitive": { 120 | "version": "1.2.1", 121 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", 122 | "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", 123 | "dependencies": { 124 | "is-callable": "^1.1.4", 125 | "is-date-object": "^1.0.1", 126 | "is-symbol": "^1.0.2" 127 | }, 128 | "engines": { 129 | "node": ">= 0.4" 130 | }, 131 | "funding": { 132 | "url": "https://github.com/sponsors/ljharb" 133 | } 134 | }, 135 | "node_modules/extend": { 136 | "version": "3.0.2", 137 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 138 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 139 | }, 140 | "node_modules/fs-extra": { 141 | "version": "8.1.0", 142 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", 143 | "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", 144 | "dependencies": { 145 | "graceful-fs": "^4.2.0", 146 | "jsonfile": "^4.0.0", 147 | "universalify": "^0.1.0" 148 | }, 149 | "engines": { 150 | "node": ">=6 <7 || >=8" 151 | } 152 | }, 153 | "node_modules/fs.realpath": { 154 | "version": "1.0.0", 155 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 156 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 157 | }, 158 | "node_modules/function-bind": { 159 | "version": "1.1.1", 160 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 161 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 162 | }, 163 | "node_modules/glob": { 164 | "version": "7.1.6", 165 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 166 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 167 | "deprecated": "Glob versions prior to v9 are no longer supported", 168 | "dependencies": { 169 | "fs.realpath": "^1.0.0", 170 | "inflight": "^1.0.4", 171 | "inherits": "2", 172 | "minimatch": "^3.0.4", 173 | "once": "^1.3.0", 174 | "path-is-absolute": "^1.0.0" 175 | }, 176 | "engines": { 177 | "node": "*" 178 | }, 179 | "funding": { 180 | "url": "https://github.com/sponsors/isaacs" 181 | } 182 | }, 183 | "node_modules/graceful-fs": { 184 | "version": "4.2.3", 185 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", 186 | "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" 187 | }, 188 | "node_modules/has": { 189 | "version": "1.0.3", 190 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 191 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 192 | "dependencies": { 193 | "function-bind": "^1.1.1" 194 | }, 195 | "engines": { 196 | "node": ">= 0.4.0" 197 | } 198 | }, 199 | "node_modules/has-symbols": { 200 | "version": "1.0.1", 201 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", 202 | "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", 203 | "engines": { 204 | "node": ">= 0.4" 205 | }, 206 | "funding": { 207 | "url": "https://github.com/sponsors/ljharb" 208 | } 209 | }, 210 | "node_modules/inflight": { 211 | "version": "1.0.6", 212 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 213 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 214 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 215 | "dependencies": { 216 | "once": "^1.3.0", 217 | "wrappy": "1" 218 | } 219 | }, 220 | "node_modules/inherits": { 221 | "version": "2.0.4", 222 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 223 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 224 | }, 225 | "node_modules/is-callable": { 226 | "version": "1.1.5", 227 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", 228 | "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", 229 | "engines": { 230 | "node": ">= 0.4" 231 | }, 232 | "funding": { 233 | "url": "https://github.com/sponsors/ljharb" 234 | } 235 | }, 236 | "node_modules/is-date-object": { 237 | "version": "1.0.2", 238 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", 239 | "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", 240 | "engines": { 241 | "node": ">= 0.4" 242 | }, 243 | "funding": { 244 | "url": "https://github.com/sponsors/ljharb" 245 | } 246 | }, 247 | "node_modules/is-regex": { 248 | "version": "1.0.5", 249 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", 250 | "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", 251 | "dependencies": { 252 | "has": "^1.0.3" 253 | }, 254 | "engines": { 255 | "node": ">= 0.4" 256 | }, 257 | "funding": { 258 | "url": "https://github.com/sponsors/ljharb" 259 | } 260 | }, 261 | "node_modules/is-symbol": { 262 | "version": "1.0.3", 263 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", 264 | "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", 265 | "dependencies": { 266 | "has-symbols": "^1.0.1" 267 | }, 268 | "engines": { 269 | "node": ">= 0.4" 270 | }, 271 | "funding": { 272 | "url": "https://github.com/sponsors/ljharb" 273 | } 274 | }, 275 | "node_modules/jsonfile": { 276 | "version": "4.0.0", 277 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 278 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 279 | "optionalDependencies": { 280 | "graceful-fs": "^4.1.6" 281 | } 282 | }, 283 | "node_modules/minimatch": { 284 | "version": "3.0.4", 285 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 286 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 287 | "dependencies": { 288 | "brace-expansion": "^1.1.7" 289 | }, 290 | "engines": { 291 | "node": "*" 292 | } 293 | }, 294 | "node_modules/node-fetch": { 295 | "version": "2.6.1", 296 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 297 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 298 | "engines": { 299 | "node": "4.x || >=6.0.0" 300 | } 301 | }, 302 | "node_modules/object-inspect": { 303 | "version": "1.7.0", 304 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", 305 | "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", 306 | "funding": { 307 | "url": "https://github.com/sponsors/ljharb" 308 | } 309 | }, 310 | "node_modules/object-keys": { 311 | "version": "1.1.1", 312 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 313 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 314 | "engines": { 315 | "node": ">= 0.4" 316 | } 317 | }, 318 | "node_modules/object.assign": { 319 | "version": "4.1.0", 320 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 321 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 322 | "dependencies": { 323 | "define-properties": "^1.1.2", 324 | "function-bind": "^1.1.1", 325 | "has-symbols": "^1.0.0", 326 | "object-keys": "^1.0.11" 327 | }, 328 | "engines": { 329 | "node": ">= 0.4" 330 | } 331 | }, 332 | "node_modules/once": { 333 | "version": "1.4.0", 334 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 335 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 336 | "dependencies": { 337 | "wrappy": "1" 338 | } 339 | }, 340 | "node_modules/path-is-absolute": { 341 | "version": "1.0.1", 342 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 343 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 344 | "engines": { 345 | "node": ">=0.10.0" 346 | } 347 | }, 348 | "node_modules/promise.prototype.finally": { 349 | "version": "3.1.2", 350 | "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz", 351 | "integrity": "sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==", 352 | "dependencies": { 353 | "define-properties": "^1.1.3", 354 | "es-abstract": "^1.17.0-next.0", 355 | "function-bind": "^1.1.1" 356 | }, 357 | "engines": { 358 | "node": ">= 0.4" 359 | }, 360 | "funding": { 361 | "url": "https://github.com/sponsors/ljharb" 362 | } 363 | }, 364 | "node_modules/string.prototype.trimleft": { 365 | "version": "2.1.1", 366 | "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", 367 | "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", 368 | "dependencies": { 369 | "define-properties": "^1.1.3", 370 | "function-bind": "^1.1.1" 371 | }, 372 | "engines": { 373 | "node": ">= 0.4" 374 | }, 375 | "funding": { 376 | "url": "https://github.com/sponsors/ljharb" 377 | } 378 | }, 379 | "node_modules/string.prototype.trimright": { 380 | "version": "2.1.1", 381 | "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", 382 | "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", 383 | "dependencies": { 384 | "define-properties": "^1.1.3", 385 | "function-bind": "^1.1.1" 386 | }, 387 | "engines": { 388 | "node": ">= 0.4" 389 | }, 390 | "funding": { 391 | "url": "https://github.com/sponsors/ljharb" 392 | } 393 | }, 394 | "node_modules/thorin-sanitize": { 395 | "version": "1.1.6", 396 | "resolved": "https://registry.npmjs.org/thorin-sanitize/-/thorin-sanitize-1.1.6.tgz", 397 | "integrity": "sha512-VxcZcpG92VNS2oF/P/a1BfEzK/GuC2EetQOAwtCgj7vVxmHsZFIghaiXA74kVjfvrGAfZ+ibcqhNPn4N0qcxwA==", 398 | "hasShrinkwrap": true, 399 | "dependencies": { 400 | "is-valid-path": "0.1.1", 401 | "range_check": "1.4.0", 402 | "validator": "13.7.0" 403 | } 404 | }, 405 | "node_modules/thorin-sanitize/node_modules/ip6": { 406 | "version": "0.0.4", 407 | "resolved": "https://registry.npmjs.org/ip6/-/ip6-0.0.4.tgz", 408 | "integrity": "sha1-RMWp23njnUBSAbTXjROzhw5I2zE=" 409 | }, 410 | "node_modules/thorin-sanitize/node_modules/ipaddr.js": { 411 | "version": "1.2.0", 412 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.2.0.tgz", 413 | "integrity": "sha1-irpJyRknmVhb3WQ+DMtQ6K53e6Q=", 414 | "engines": { 415 | "node": ">= 0.10" 416 | } 417 | }, 418 | "node_modules/thorin-sanitize/node_modules/is-extglob": { 419 | "version": "1.0.0", 420 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", 421 | "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", 422 | "engines": { 423 | "node": ">=0.10.0" 424 | } 425 | }, 426 | "node_modules/thorin-sanitize/node_modules/is-glob": { 427 | "version": "2.0.1", 428 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", 429 | "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", 430 | "dependencies": { 431 | "is-extglob": "^1.0.0" 432 | }, 433 | "engines": { 434 | "node": ">=0.10.0" 435 | } 436 | }, 437 | "node_modules/thorin-sanitize/node_modules/is-invalid-path": { 438 | "version": "0.1.0", 439 | "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", 440 | "integrity": "sha1-MHqFWzzxqTi0TqcNLGEQYFNxTzQ=", 441 | "dependencies": { 442 | "is-glob": "^2.0.0" 443 | }, 444 | "engines": { 445 | "node": ">=0.10.0" 446 | } 447 | }, 448 | "node_modules/thorin-sanitize/node_modules/is-valid-path": { 449 | "version": "0.1.1", 450 | "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", 451 | "integrity": "sha1-EQ+f90w39mPh7HkV60UfLbk6yd8=", 452 | "dependencies": { 453 | "is-invalid-path": "^0.1.0" 454 | }, 455 | "engines": { 456 | "node": ">=0.10.0" 457 | } 458 | }, 459 | "node_modules/thorin-sanitize/node_modules/range_check": { 460 | "version": "1.4.0", 461 | "resolved": "https://registry.npmjs.org/range_check/-/range_check-1.4.0.tgz", 462 | "integrity": "sha1-zYfHrGLEC6nfabhwPGBPYMN0hjU=", 463 | "dependencies": { 464 | "ip6": "0.0.4", 465 | "ipaddr.js": "1.2" 466 | } 467 | }, 468 | "node_modules/thorin-sanitize/node_modules/validator": { 469 | "version": "13.7.0", 470 | "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", 471 | "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", 472 | "engines": { 473 | "node": ">= 0.10" 474 | } 475 | }, 476 | "node_modules/universalify": { 477 | "version": "0.1.2", 478 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 479 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", 480 | "engines": { 481 | "node": ">= 4.0.0" 482 | } 483 | }, 484 | "node_modules/uuid": { 485 | "version": "3.4.0", 486 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 487 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 488 | "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", 489 | "bin": { 490 | "uuid": "bin/uuid" 491 | } 492 | }, 493 | "node_modules/wrappy": { 494 | "version": "1.0.2", 495 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 496 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 497 | } 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thorin", 3 | "author": "UNLOQ Systems", 4 | "version": "1.5.8", 5 | "dependencies": { 6 | "async": "3.1.1", 7 | "camelcase": "5.3.1", 8 | "colors": "1.4.0", 9 | "dot-object": "2.1.2", 10 | "extend": "3.0.2", 11 | "fs-extra": "8.1.0", 12 | "node-fetch": "2.6.1", 13 | "promise.prototype.finally": "3.1.2", 14 | "thorin-sanitize": "^1.1.6", 15 | "uuid": "3.4.0" 16 | }, 17 | "description": "Next gen framework with a cool sound", 18 | "main": "lib/Thorin.js", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/Thorinjs/Thorin" 23 | }, 24 | "homepage": "http://thorinjs.com", 25 | "bugs": { 26 | "url": "https://github.com/Thorinjs/Thorin/issues" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spawn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * This is a simple wrapper over child_process.spawn 4 | * */ 5 | const {spawn} = require('child_process'), 6 | path = require('path'); 7 | 8 | /** 9 | * The spawn function that will launch a new node.js process 10 | * via spawn, not cluster. The current working directory will 11 | * be used to search for the script 12 | * */ 13 | module.exports = function () { 14 | let args = Array.prototype.slice.call(arguments); 15 | if (args.length === 0) throw new Error('Please provide the script to run'); 16 | let scriptName = args.splice(0, 1)[0]; 17 | if (typeof scriptName !== 'string' || !scriptName) throw new Error('Please provide the script name to run'); 18 | let fullPath = process.cwd(); 19 | let opt = (typeof args[args.length - 1] === 'object' && args[args.length - 1]) ? args.pop() : {}; 20 | if (!opt.cwd) opt.cwd = fullPath; 21 | if (!opt.env) opt.env = process.env; 22 | let timeout = (opt.timeout || 10000), 23 | command = opt.command || 'node'; 24 | delete opt.command; 25 | delete opt.timeout; 26 | let _log = (d) => { 27 | d = d.toString().replace(/\n+/g, '\n'); 28 | if (d.trim() === '') return; 29 | if (d.charAt(d.length - 1) === '\n') d = d.substr(0, d.length - 1); 30 | console.log(d); 31 | }; 32 | let sargs = [scriptName].concat(args); 33 | let ps = spawn(command, sargs, opt); 34 | ps.stdout.on('data', _log); 35 | ps.stderr.on('data', _log); 36 | return new Promise((resolve, reject) => { 37 | let isDone = false; 38 | let _timer = setTimeout(() => { 39 | if (isDone) return; 40 | isDone = true; 41 | ps.kill('SIGHUP'); 42 | }, timeout); 43 | ps.once('close', (code) => { 44 | clearTimeout(_timer); 45 | if (isDone) return; 46 | isDone = true; 47 | if (code === 0) { 48 | return resolve(); 49 | } 50 | return reject(new Error(`Exit with code: ${code}`)); 51 | }); 52 | ps.once('error', (e) => { 53 | clearTimeout(_timer); 54 | if (isDone) return; 55 | isDone = true; 56 | reject(e); 57 | }); 58 | }); 59 | } 60 | 61 | /** 62 | * Simple function that will just wait the given time and resolve the promise 63 | * */ 64 | module.exports.wait = (ms) => { 65 | return new Promise((resolve) => { 66 | setTimeout(() => { 67 | resolve(); 68 | }, ms || 1000); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'), 3 | path = require('path'), 4 | exec = require('child_process').exec, 5 | cwd = process.cwd(); 6 | 7 | const INSTALL_MATCH = ['thorin*'], 8 | NEW_INSTALLS = [], 9 | IS_PROD = process.env.NODE_ENV === 'production'; 10 | 11 | /** 12 | * This script does a thorin-* auto-update to latest version. 13 | * This can also perform update to the latest version of the argv modules. 14 | * 15 | * Ex: 16 | * node node_modules/thorin/update mymodule1 mymodule2@latest 17 | * */ 18 | function doUpdate(deps = []) { 19 | if (NEW_INSTALLS.length === 0) { 20 | console.log(`--> No new installs are required.`); 21 | return process.exit(0); 22 | } 23 | console.log(`--> Updating Thorin modules to latest version:`); 24 | const npmInstalls = []; 25 | NEW_INSTALLS.forEach((item) => { 26 | if (item.current && item.current.indexOf('.') !== -1) { 27 | item.version = item.current.split('.')[0] + '.x'; 28 | } 29 | let msg = `- ${item.name}`; 30 | if (item.current) msg += `@${item.current}`; 31 | msg += ` -> ${item.version}`; 32 | npmInstalls.push(`${item.name}@${item.version}`); 33 | console.log(msg); 34 | }); 35 | let cmd = ['npm install']; 36 | if (IS_PROD) { 37 | cmd.push(`--only=prod`); 38 | } 39 | cmd.push(`--no-optional`); 40 | cmd = cmd.concat(npmInstalls); 41 | cmd = cmd.join(' '); 42 | console.log(`--> Running: ${cmd}`); 43 | return new Promise((resolve, reject) => { 44 | exec(cmd, { 45 | cwd: cwd, 46 | env: process.env, 47 | maxBuffer: 1024 * 1024 * 512 48 | }, (err, stdout, stderr) => { 49 | console.log(`--> Thorin updater results\n\n`); 50 | if (stdout) console.log(stdout); 51 | if (stderr) console.log(stderr); 52 | if (err) return reject(err); 53 | resolve(); 54 | }); 55 | }); 56 | } 57 | 58 | function processArgv() { 59 | const argv = process.argv; 60 | if (argv.length <= 2) return; 61 | for (let i = 2; i < argv.length; i++) { 62 | let v = argv[i].trim(); 63 | if (v.indexOf('*') !== -1) { 64 | INSTALL_MATCH.push(v); 65 | continue; 66 | } 67 | if (v.indexOf('/') !== -1) { // we have ns. 68 | let t = v.split('/').pop(); 69 | if (t.indexOf('@') === -1) v += '@latest'; 70 | } else { 71 | if (v.indexOf('@') === -1) v += '@latest'; 72 | } 73 | let q = v.lastIndexOf('@'); 74 | let name = v.substr(0, q), 75 | version = v.substr(q + 1); 76 | 77 | NEW_INSTALLS.push({ 78 | name, 79 | version 80 | }); 81 | } 82 | } 83 | 84 | function processDeps(deps) { 85 | let names = Object.keys(deps || {}); 86 | for (let i = 0; i < names.length; i++) { 87 | let name = names[i], 88 | ver = deps[name], 89 | found = match(name); 90 | if (!found) continue; 91 | NEW_INSTALLS.push({ 92 | name, 93 | current: ver || '', 94 | version: 'latest' 95 | }); 96 | } 97 | } 98 | 99 | (async () => { 100 | let pkgPath = path.normalize(`${cwd}/package.json`), 101 | pkgInfo; 102 | try { 103 | pkgInfo = require(pkgPath); 104 | } catch (e) { 105 | console.error(`--> Could not read package.json from: ${pkgPath}`); 106 | console.log(e); 107 | return process.exit(1); 108 | } 109 | processArgv(); 110 | processDeps(pkgInfo.dependencies); 111 | try { 112 | await doUpdate(); 113 | } catch (e) { 114 | console.error(`--> Could not finalize update`); 115 | console.log(e); 116 | return process.exit(1); 117 | } 118 | console.log(`--> Completed`); 119 | })(); 120 | 121 | function match(name) { 122 | for (let i = 0; i < INSTALL_MATCH.length; i++) { 123 | let p = INSTALL_MATCH[i]; 124 | let isEnd = p.charAt(0) === '*', 125 | isStart = p.charAt(p.length - 1) === '*', 126 | m = p.replace('*', ''); 127 | if (isStart && name.indexOf(m) === 0) { 128 | return true; 129 | } 130 | if (isEnd && name.substr(m.length) === m) { 131 | return true; 132 | } 133 | } 134 | return false; 135 | } 136 | --------------------------------------------------------------------------------