UNPKG

14 kBJavaScriptView Raw
1/*!
2 * Copyright (c) 2012-2019 Digital Bazaar, Inc. All rights reserved.
3 */
4'use strict';
5
6const config = require('./config');
7const delay = require('delay');
8const lodashGet = require('lodash.get');
9const lodashSet = require('lodash.set');
10const lodashTemplate = require('lodash.template');
11const lodashToPath = require('lodash.topath');
12const util = require('util');
13const uuid = require('uuid-random');
14
15const api = {};
16module.exports = api;
17
18/**
19 * Create a promise which resolves after the specified milliseconds.
20 *
21 * @see {@link https://github.com/sindresorhus/delay}
22 */
23api.delay = delay;
24
25// BedrockError class
26api.BedrockError = function(message, type, details, cause) {
27 Error.call(this, message);
28 Error.captureStackTrace(this, this.constructor);
29 this.name = type;
30 this.message = message;
31 this.details = details || null;
32 this.cause = cause || null;
33};
34util.inherits(api.BedrockError, Error);
35api.BedrockError.prototype.name = 'BedrockError';
36api.BedrockError.prototype.toObject = function(options) {
37 options = options || {};
38 options.public = options.public || false;
39
40 // convert error to object
41 const rval = _toObject(this, options);
42
43 // add stack trace only for non-public development conversion
44 if(!options.public && config.core.errors.showStack) {
45 // try a basic parse
46 rval.stack = _parseStack(this.stack);
47 }
48
49 return rval;
50};
51// check type of this error
52api.BedrockError.prototype.isType = function(type) {
53 return api.hasValue(this, 'name', type);
54};
55// check type of this error or one of it's causes
56api.BedrockError.prototype.hasType = function(type) {
57 return this.isType(type) || this.hasCauseOfType(type);
58};
59// check type of error cause or one of it's causes
60api.BedrockError.prototype.hasCauseOfType = function(type) {
61 if(this.cause && this.cause instanceof api.BedrockError) {
62 return this.cause.hasType(type);
63 }
64 return false;
65};
66
67const _genericErrorJSON = {
68 message: 'An internal server error occurred.',
69 type: 'bedrock.InternalServerError'
70};
71
72const _errorMessageRegex = /^Error:\s*/;
73const _errorAtRegex = /^\s+at\s*/;
74/**
75 * Parse an Error stack property into a JSON structure.
76 *
77 * NOTE: Uses some format heuristics and may be fooled by tricky errors.
78 *
79 * TODO: look into better stack parsing libraries.
80 */
81function _parseStack(stack) {
82 try {
83 const lines = stack.split('\n');
84 const messageLines = [];
85 const atLines = [];
86 for(let i = 0; i < lines.length; ++i) {
87 const line = lines[i];
88 // push location-like lines to a stack array
89 if(line.match(_errorAtRegex)) {
90 atLines.push(line.replace(_errorAtRegex, ''));
91 } else {
92 // push everything else to a message array
93 messageLines.push(line.replace(_errorMessageRegex, ''));
94 }
95 }
96 return {
97 message: messageLines.join('\n'),
98 at: atLines
99 };
100 } catch(e) {
101 // FIXME: add parse error handling
102 return stack;
103 }
104}
105
106function _toObject(err, options) {
107 if(!err) {
108 return null;
109 }
110
111 if(options.public) {
112 // public conversion
113 // FIXME also check if a validation type?
114 if(err instanceof api.BedrockError &&
115 err.details && err.details.public) {
116 const details = api.clone(err.details);
117 delete details.public;
118 // mask cause if it is not a public bedrock error
119 let {cause} = err;
120 if(!(cause && cause instanceof api.BedrockError &&
121 cause.details && cause.details.public)) {
122 cause = null;
123 }
124 return {
125 message: err.message,
126 type: err.name,
127 details,
128 cause: _toObject(cause, options)
129 };
130 } else {
131 // non-bedrock error or not public, return generic error
132 return _genericErrorJSON;
133 }
134 } else {
135 // full private conversion
136 if(err instanceof api.BedrockError) {
137 return {
138 message: err.message,
139 type: err.name,
140 details: err.details,
141 cause: _toObject(err.cause, options)
142 };
143 } else {
144 return {
145 message: err.message,
146 type: err.name,
147 details: {
148 inspect: util.inspect(err, false, 10),
149 stack: _parseStack(err.stack)
150 },
151 cause: null
152 };
153 }
154 }
155}
156
157/**
158 * Gets the passed date in W3C format (eg: 2011-03-09T21:55:41Z).
159 *
160 * @param date the date.
161 *
162 * @return the date in W3C format.
163 */
164api.w3cDate = function(date) {
165 if(date === undefined || date === null) {
166 date = new Date();
167 } else if(typeof date === 'number' || typeof date === 'string') {
168 date = new Date(date);
169 }
170 return util.format('%d-%s-%sT%s:%s:%sZ',
171 date.getUTCFullYear(),
172 _zeroFill(date.getUTCMonth() + 1),
173 _zeroFill(date.getUTCDate()),
174 _zeroFill(date.getUTCHours()),
175 _zeroFill(date.getUTCMinutes()),
176 _zeroFill(date.getUTCSeconds()));
177};
178
179function _zeroFill(num) {
180 return (num < 10) ? '0' + num : '' + num;
181}
182
183/**
184 * Merges the contents of one or more objects into the first object.
185 *
186 * @param deep (optional), true to do a deep-merge.
187 * @param target the target object to merge properties into.
188 * @param objects N objects to merge into the target.
189 *
190 * @return the default Bedrock JSON-LD context.
191 */
192api.extend = function() {
193 let deep = false;
194 let i = 0;
195 if(arguments.length > 0 && typeof arguments[0] === 'boolean') {
196 deep = arguments[0];
197 ++i;
198 }
199 const target = arguments[i] || {};
200 i++;
201 for(; i < arguments.length; ++i) {
202 const obj = arguments[i] || {};
203 Object.keys(obj).forEach(function(name) {
204 const value = obj[name];
205 if(deep && api.isObject(value) && !Array.isArray(value)) {
206 target[name] = api.extend(true, target[name], value);
207 } else {
208 target[name] = value;
209 }
210 });
211 }
212 return target;
213};
214
215/**
216 * Returns true if the given value is an Object.
217 *
218 * @param value the value to check.
219 *
220 * @return true if it is an Object, false if not.
221 */
222api.isObject = function(value) {
223 return (Object.prototype.toString.call(value) === '[object Object]');
224};
225
226/**
227 * Clones a value. If the value is an array or an object it will be deep cloned.
228 *
229 * @param value the value to clone.
230 *
231 * @return the clone.
232 */
233api.clone = function(value) {
234 if(value && typeof value === 'object') {
235 let rval;
236 if(Array.isArray(value)) {
237 rval = new Array(value.length);
238 for(let i = 0; i < rval.length; i++) {
239 rval[i] = api.clone(value[i]);
240 }
241 } else {
242 rval = {};
243 for(const j in value) {
244 rval[j] = api.clone(value[j]);
245 }
246 }
247 return rval;
248 }
249 return value;
250};
251
252// config utilities
253
254// config namespace
255api.config = {};
256
257// check if argument looks like a string or array path
258function _isPath(maybePath) {
259 return typeof maybePath === 'string' || Array.isArray(maybePath);
260}
261// set default for path if it does not exist
262function _setDefault(object, path, value) {
263 // ensure path is array
264 if(typeof path === 'string') {
265 path = lodashToPath(path);
266 }
267 if(path.length) {
268 let target = lodashGet(object, path);
269 if(!target) {
270 target = value;
271 lodashSet(object, path, target);
272 }
273 return target;
274 } else {
275 return object;
276 }
277}
278
279/**
280 * Wrapper with helpers for config objects.
281 *
282 * @param object the config object.
283 * @param [options] options to use:
284 * config: parent config object
285 * locals: object containing variables used for string templates.
286 * Defaults to main config object.
287 */
288api.config.Config = function(object, options) {
289 this.object = object;
290 this.options = options || {};
291};
292
293/**
294 * Set a path to a value.
295 *
296 * Multiple paths can be set at once with an object with many string path keys
297 * and associated values.
298 *
299 * @param path lodash-style string or array path, or an object with many path
300 * key and value pairs.
301 * @param value value to set at the path when using single path.
302 */
303api.config.Config.prototype.set = function(path, value) {
304 if(!_isPath(path)) {
305 Object.keys(path).forEach(key => lodashSet(this.object, key, path[key]));
306 return;
307 }
308 lodashSet(this.object, path, value);
309};
310
311/**
312 * Set a path to a default value if it does not exist. All elements of the path
313 * will be initialized as an empty object if they do not exist.
314 *
315 * Multiple paths can be set at once with an object with many string path keys
316 * and associated default values;
317 *
318 * Note: To initialize the final element of a path to the empty object even if
319 * it already exists, use c.set(path, {});
320 *
321 * @param path lodash-style string or array path, or an object with many path
322 * key and default value pairs.
323 * @param value default value to set at the path when using a single path.
324 * @return the last element of the path or a path indexed object with element
325 * values.
326 */
327api.config.Config.prototype.setDefault = function(path, value) {
328 if(!_isPath(path)) {
329 const paths = {};
330 Object.keys(path).forEach(key => {
331 paths[key] = _setDefault(this.object, key, path[key]);
332 });
333 return paths;
334 }
335 return _setDefault(this.object, path, value);
336};
337
338/**
339 * Assigns a getter to a config path. When the config path is read, the getter
340 * will execute and compute the configured value. This is particularly useful
341 * for config values that depend on other config values; it removes the need
342 * to update such a value when its dependencies change.
343 *
344 * The value can be computed from a function or from a lodash template that
345 * will be evaluated using `bedrock.config` for its local variables.
346 *
347 * @param path lodash-style string or array path, or an object with many path
348 * key and value pairs.
349 * @param fnOrExpression a lodash template or a function used to compute the
350 * path value.
351 * @param [options] options to use:
352 * locals: object containing variables used for string templates.
353 * parentDefault: value for parent if it does not exist.
354 */
355api.config.Config.prototype.setComputed =
356 function(path, fnOrExpression, options) {
357 if(!_isPath(path)) {
358 options = fnOrExpression;
359 Object.keys(path).forEach(key => this.setComputed(
360 key, path[key], options));
361 return;
362 }
363 if(typeof fnOrExpression === 'string') {
364 // handle strings as templates
365 fnOrExpression = lodashTemplate(fnOrExpression);
366 } else if(typeof fnOrExpression !== 'function') {
367 // handle non-string non-functions as simple values
368 return this.set(path, fnOrExpression);
369 }
370 // ensure path is array
371 if(typeof path === 'string') {
372 path = lodashToPath(path);
373 }
374 // locals
375 options = options || {};
376 const locals = options.locals || this.options.locals || config;
377 // get target object path
378 const targetPath = path.slice(0, -1);
379 // get key
380 const targetKey = path.slice(-1);
381 // get or create target parent object
382 const parentDefault = options.parentDefault || {};
383 const target = _setDefault(this.object, targetPath, parentDefault);
384 // setup property
385 let _isSet = false;
386 let _value;
387 Object.defineProperty(target, targetKey, {
388 configurable: true,
389 enumerable: true,
390 get: () => {
391 if(_isSet) {
392 return _value;
393 }
394 return fnOrExpression(locals);
395 },
396 set: value => {
397 _isSet = true;
398 _value = value;
399 }
400 });
401 };
402
403/**
404 * Create a bound setComputed function for this Config instance. Used to
405 * simplify code.
406 *
407 * let cc = bedrock.util.config.main.computer();
408 * cc('...', ...);
409 *
410 * @return bound setComputed function.
411 */
412api.config.Config.prototype.computer = function() {
413 return this.setComputed.bind(this);
414};
415
416/**
417 * Push a getter to an array specified by a config path. See setComputed for an
418 * explaination of how getters work.
419 *
420 * @param path lodash-style string or array path.
421 * @param fnOrExpression a lodash template or a function used to compute the
422 * path value.
423 * @param [options] options to use:
424 * locals: object containing variables used for string templates.
425 */
426api.config.Config.prototype.pushComputed =
427 function(path, fnOrExpression, options) {
428 // get target or default array
429 const target = lodashGet(this.object, path, []);
430 // add next index
431 const pushPath = lodashToPath(path);
432 pushPath.push(target.length);
433 // use default parent array
434 const pushOptions = Object.assign({}, options, {parentDefault: []});
435 // set computed array element
436 this.setComputed(pushPath, fnOrExpression, pushOptions);
437 };
438
439/**
440 * Shared wrapper for the standard bedrock config.
441 */
442api.config.main = new api.config.Config(config);
443
444/**
445 * Generates a new v4 UUID.
446 *
447 * @return the new v4 UUID.
448 */
449api.uuid = uuid;
450
451/**
452 * Parse the string or value and return a boolean value or raise an exception.
453 * Handles true and false booleans and case-insensitive 'yes', 'no', 'true',
454 * 'false', 't', 'f', '0', '1' strings.
455 *
456 * @param value a string of value.
457 *
458 * @return the boolean conversion of the value.
459 */
460api.boolify = function(value) {
461 if(typeof value === 'boolean') {
462 return value;
463 }
464 if(typeof value === 'string' && value) {
465 switch(value.toLowerCase()) {
466 case 'true':
467 case 't':
468 case '1':
469 case 'yes':
470 case 'y':
471 return true;
472 case 'false':
473 case 'f':
474 case '0':
475 case 'no':
476 case 'n':
477 return false;
478 }
479 }
480 // if here we couldn't parse it
481 throw new Error('Invalid boolean:' + value);
482};
483
484api.callbackify = fn => {
485 const callbackVersion = util.callbackify(fn);
486 return function(...args) {
487 const callback = args[args.length - 1];
488 if(typeof callback === 'function') {
489 return callbackVersion.apply(null, args);
490 }
491 return fn.apply(null, args);
492 };
493};
494
495// a replacement for jsonld.hasValue
496api.hasValue = (obj, key, value) => {
497 const t = obj[key];
498 if(Array.isArray(t)) {
499 return t.includes(value);
500 }
501 return t === value;
502};