UNPKG

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