UNPKG

14.7 kBJavaScriptView Raw
1'use strict';
2var util = require('util')
3 , events = require('events')
4 , mergeRecursive = require('merge-recursive')
5 , objectMerge = require('object-merge')
6 , Validate = require('./Validate')
7 , ValidateHelpers = require('./ValidateHelpers')
8 , StorageDriver = require('./StorageDriver')
9 , uuid = require('uuid')
10
11/**
12 * Take a path argument and normalize to something usable
13 * from multiple types
14 * @param path
15 * @returns {*}
16 */
17var pathNormalize = function(path){
18 if(undefined === path){
19 throw new Error('path is required')
20 }
21 if(util.isArray(path)){
22 return path.join('.')
23 }
24 if('function' === typeof path){
25 return pathNormalize(path())
26 }
27 if('object' === typeof path && 'string' !== typeof path && 'function' === typeof path.toString){
28 return path.toString()
29 }
30 return path
31}
32
33/**
34 * Private function to traverse the target and return
35 * the path
36 * @param target
37 * @param path
38 * @returns {*}
39 */
40var get = function(target,path){
41 if('object' !== typeof target) return undefined
42 if(undefined === path) return target
43 path = pathNormalize(path)
44 var o = target
45 , p = path.split('.')
46 , i, a
47 for(i = 0; o && (a = p[i++]); o = o[a]){
48 if(undefined === o[a]) return undefined
49 }
50 return o
51}
52
53/**
54 * Private function to traverse the target check if path exists
55 * the path
56 * @param target
57 * @param path
58 * @returns {boolean}
59 */
60var exists = function(target,path){
61 if('object' !== typeof target) return false
62 path = pathNormalize(path)
63 var o = target
64 , p = path.split('.')
65 , i, a
66 for(i = 0; o && (a = p[i++]); o = o[a]){
67 if(!o.hasOwnProperty(a)) return false
68 }
69 return true
70}
71
72/**
73 * Private function traverse the target and set the value
74 * to the path
75 * @param target
76 * @param path
77 * @param value
78 */
79var set = function(target,path,value){
80 path = pathNormalize(path)
81 if('object' === typeof target){
82 var o = target
83 for(var a,p=path.split('.'),i=0; o&&(a=p[i++]); o=o[a]){
84 if(i !== p.length && undefined === o[a]) o[a] = {}
85 else if(i !== p.length && 'object' !== typeof o[a]) o[a] = {}
86 else if(i === p.length) o[a] = value
87 }
88 }
89}
90
91/**
92 * Private function traverse the target and remove the path
93 * to the path
94 * @param target
95 * @param path
96 * @returns {boolean}
97 */
98var remove = function(target,path){
99 path = pathNormalize(path)
100 var o = target
101 for(var a,p=path.split('.'),i=0; o&&(a=p[i++]); o=o[a]){
102 if(i !== p.length && undefined === o[a]){
103 return false
104 } else if(i === p.length){
105 o[a] = undefined
106 delete o[a]
107 return true
108 }
109 }
110 return false
111}
112
113/**
114 * Walk the object and return an array of the paths
115 * @param obj
116 * @returns {Array}
117 */
118var walkTree = function(obj){
119 var keys = [];
120 for(var key in obj){
121 if(!obj.hasOwnProperty(key)) continue
122 keys.push(key)
123 if('object' === typeof obj[key]){
124 var subKeys = walkTree(obj[key])
125 for(var i = 0; i < subKeys.length; i++){
126 keys.push(key + '.' + subKeys[i])
127 }
128 }
129 }
130 return keys
131}
132
133/**
134 * Run validation of an action
135 *
136 * @param validate Validation function
137 * @param path Path being evaluated
138 * @param value Value being evaluated
139 * @param emit Callback to emit events
140 * @param pre Callback before emit
141 * @param post Callback after emit for return
142 * @returns {*}
143 */
144var runValidation = function(validate,path,value,emit,pre,post){
145 var err = null
146 , self = this
147 if('string' === typeof emit){
148 var event = emit
149 emit = function(verb,message,path,value){
150 if('ok' === verb || 'warn' === verb){
151 self.emit(event,path,value)
152 }
153 if('ok' !== verb){
154 self.emit(verb,event,message,path,value)
155 }
156 }
157 }
158 if(!pre) pre = function(verb,value){return value}
159 if(!post) post = function(verb,value){return value}
160 if('function' !== typeof validate){
161 value = pre('ok',value)
162 emit('ok','',path,value)
163 return post('ok',value)
164 } else {
165 try {
166 value = validate.call(new ValidateHelpers(path,value),path,value)
167 } catch(e){
168 if(!(e instanceof Validate)){
169 throw e
170 } else {
171 err = e
172 }
173 }
174 if(null === err || 'ok' === err.verb){
175 if(null !== err && 'ok' === err.verb) value = err.value
176 value = pre('ok',value)
177 emit('ok','',path,value)
178 return post('ok',value)
179 } else if('warn' === err.verb){
180 value = pre('warn',value)
181 emit('warn',err.message,path,value)
182 return post('warn',value)
183 } else if('drop' === err.verb){
184 emit('drop',err.message,path,value)
185 return post('drop',value)
186 } else if('reject' === err.verb){
187 emit('reject',err.message,path,value)
188 return post('reject',value)
189 } else {
190 emit('error',err.message,path,value)
191 throw new Error(err.message)
192 }
193 }
194}
195
196/**
197 * Manage constructor passes the initial argument to ObjectManage.load()
198 * and accepts the same set of parameters
199 * Takes any number of Objects to be loaded and merged at call time
200 * @constructor
201 */
202var ObjectManage = function(){
203 events.EventEmitter.call(this)
204 this.data = {}
205 //send any arguments to be merged into the main data object
206 this.load.apply(this,arguments)
207 //setup the memory driver as the default
208 var MemoryDriver = require('../drivers/memory')
209 this.driver = new MemoryDriver()
210}
211util.inherits(ObjectManage,events.EventEmitter)
212
213/**
214 * The object to manage
215 * @type {{}}
216 */
217ObjectManage.prototype.data = {}
218
219/**
220 * Maximum amount of depth to allow when merging
221 * @type {number}
222 */
223ObjectManage.prototype.maxDepth = 50
224
225/**
226 * Reference to the Validate object
227 * @type {Function}
228 */
229ObjectManage.Validate = Validate
230
231/**
232 * Set respective path to value
233 * @param path
234 * @param value
235 */
236ObjectManage.prototype.set = function(path,value){
237 var self = this
238 return runValidation.call(
239 self,
240 self.validateSet,
241 path,
242 value,
243 'set',
244 function(verb,value){
245 if('ok' === verb || 'warn' === verb || 'drop' === verb){
246 set(self.data,path,value)
247 }
248 return value
249 },
250 function(verb){
251 return ('ok' === verb || 'warn' === verb || 'drop' === verb)
252 }
253 )
254}
255
256/**
257 * Validation for set directives
258 * @type {null}
259 */
260ObjectManage.prototype.validateSet = null
261
262/**
263 * Get value of path returns undefined if path does not exist
264 * @param path
265 * @returns {}
266 */
267ObjectManage.prototype.get = function(path){
268 var self = this
269 return runValidation.call(
270 self,
271 self.validateGet,
272 path,
273 get(self.data,path),
274 'get'
275 )
276}
277
278/**
279 * Validation for get directives
280 * directly
281 * @type {null}
282 */
283ObjectManage.prototype.validateGet = null
284
285/**
286 * Check if path exists (uses hasOwnProperty and is safe to having undefined set as a value)
287 * @param path
288 * @returns {boolean}
289 */
290ObjectManage.prototype.exists = function(path){
291 var self = this
292 return runValidation.call(
293 self,
294 self.validateExists,
295 path,
296 null,
297 'exists',
298 function(verb){
299 if('ok' === verb || 'warn' === verb){
300 return exists(self.data,path)
301 } else {
302 return false
303 }
304 }
305 )
306}
307
308/**
309 * Validation of exists directives
310 * @type {null}
311 */
312ObjectManage.prototype.validateExists = null
313
314/**
315 * Remove value and children at desired path (does this by deleting the value)
316 * @param path
317 * @returns {boolean}
318 */
319ObjectManage.prototype.remove = function(path){
320 var self = this
321 return runValidation.call(
322 self,
323 self.validateRemove,
324 path,
325 null,
326 'remove',
327 function(verb,value){
328 if('ok' === verb || 'warning' === verb){
329 return remove(self.data,path)
330 } else {
331 return value
332 }
333 }
334 )
335}
336
337/**
338 * Get the paths of the object in dot notation in an array
339 * @returns {Array}
340 */
341ObjectManage.prototype.getPaths = function(){
342 return walkTree(this.data)
343}
344
345/**
346 * Validation of removal directives
347 * @type {null}
348 */
349ObjectManage.prototype.validateRemove = null
350
351/**
352 * Merge arguments into data object
353 * Can be an Array or recursive Array of objects that get merged
354 * in the order they are passed
355 * Takes any number of Objects tobe merged together in order of passing
356 */
357ObjectManage.prototype.load = function(){
358 var self = this
359 , data
360 if(arguments.length > 1){
361 for(var i = 0; i < arguments.length; i++){
362 self.load(arguments[i])
363 }
364 } else {
365 data = arguments[0]
366 if('object' === typeof data){
367 var depth = self.countDepth(data)
368 if(depth >= self.maxDepth){
369 self.emit('warning',self.data,'Object being merged is too deep (' + depth +')')
370 console.log('WARN [object-manage]: Object being merged is too deep (' + depth +')')
371 console.trace()
372 }
373 runValidation.call(
374 self,
375 self.validateLoad,
376 null,
377 data,
378 function(verb,message,path,value){
379 if('ok' === verb){
380 self.emit('load',value)
381 } else {
382 self.emit(verb,'load',message,path,value)
383 }
384 },
385 function(verb,value){
386 if('ok' === verb || 'warn' === verb){
387 self.data = self.merge(self.data,value) || {}
388 return self.data
389 } else {
390 return false
391 }
392 },
393 function(verb){
394 return ('ok' === verb || 'warn' === verb)
395 }
396 )
397 }
398 }
399}
400
401/**
402 * Validation for load directives
403 * @type {null}
404 */
405ObjectManage.prototype.validateLoad = null
406
407/**
408 * Merge implementation using object-merge
409 * @param obj1
410 * @param obj2
411 * @returns {*|exports}
412 */
413ObjectManage.prototype.objectMerge = function(obj1,obj2){
414 var opts = objectMerge.createOptions({
415 depth: this.maxDepth,
416 throwOnCircularRef: true
417 })
418 return objectMerge(opts,obj1,obj2)
419}
420
421/**
422 * Merge implementation using merge-recursive
423 * @param obj1
424 * @param obj2
425 * @returns {*}
426 */
427ObjectManage.prototype.mergeRecursive = function(obj1,obj2){
428 return mergeRecursive.recursive(obj1,obj2)
429}
430
431/**
432 * Merge prototype allowed to be overirden
433 * @param obj1
434 * @param obj2
435 * @returns {*|exports}
436 */
437ObjectManage.prototype.merge = ObjectManage.prototype.objectMerge
438
439/**
440 * Count the depth of an object and return
441 * @param object Target object
442 * @returns {Number}
443 */
444ObjectManage.prototype.countDepth = function(object) {
445 var level = 1
446 var key
447 for(key in object) {
448 if (!object.hasOwnProperty(key)) continue
449 if('object' === typeof object[key]){
450 var depth = ObjectManage.prototype.countDepth(object[key]) + 1
451 level = Math.max(depth, level)
452 if(level > this.maxDepth) return this.maxDepth
453 }
454 }
455 return level
456}
457
458/**
459 * Instance of the storage backend
460 * @type {null}
461 */
462ObjectManage.prototype.driver = null
463
464/**
465 * Setup the storage backend
466 * @param driver
467 */
468ObjectManage.prototype.storage = function(driver){
469 var Driver
470 if(driver instanceof StorageDriver){
471 this.driver = driver
472 } else if('string' === typeof driver){
473 Driver = require('../drivers/' + driver)
474 this.driver = new Driver()
475 } else if('object' === typeof driver){
476 Driver = require('../drivers/' + driver.driver)
477 this.driver = new Driver(driver.options)
478 }
479 return this
480}
481
482/**
483 * Reference to the storage driver class
484 * @type {exports}
485 */
486ObjectManage.StorageDriver = require('./StorageDriver')
487
488/**
489 * Instance handle
490 * @type {null}
491 */
492ObjectManage.prototype.handle = null
493
494/**
495 * Generate Instance Handle if not already set
496 */
497ObjectManage.prototype.generateHandle = function(){
498 if(null === this.handle){
499 var handle = uuid.v4()
500 this.setHandle(handle)
501 this.emit('generateHandle',handle)
502 }
503}
504
505/**
506 * Set instance handle explicitly
507 * @param handle
508 */
509ObjectManage.prototype.setHandle = function(handle){
510 this.handle = handle
511}
512
513/**
514 * Return the instance handle that is currently set
515 * will be null if not yet generated
516 * @returns {null}
517 */
518ObjectManage.prototype.getHandle = function(){
519 return this.handle
520}
521
522/**
523 * Save to the storage backend
524 * Callback params: err, handle, data
525 * @param [cb]
526 */
527ObjectManage.prototype.save = function(cb){
528 if('function' !== typeof cb){
529 cb = function(err){
530 if(err) throw err
531 }
532 }
533 var self = this
534 self.generateHandle()
535 if('function' === typeof self.driver.save){
536 self.driver.save(self.handle,self.data,function(err){
537 self.emit('save',self.handle,self.data)
538 cb(err,self.handle,self.data)
539 })
540 } else {
541 var err = 'driver (' + self.driver.name + ') has not implemented a save method'
542 self.emit('save',err)
543 cb(err)
544 }
545}
546
547/**
548 * Restore frozen object by its handle
549 * @param handle
550 * @param [cb]
551 */
552ObjectManage.prototype.restore = function(handle,cb){
553 var self = this
554 if('function' !== typeof cb){
555 cb = function(err){
556 if(err) throw err
557 }
558 }
559 self.setHandle(handle)
560 if('function' === typeof self.driver.restore){
561 self.driver.restore(self.handle,function(err,data){
562 if(err){
563 self.emit('restore',err)
564 cb(err)
565 } else {
566 self.load(data)
567 self.emit('restore',null,data)
568 cb(null,data)
569 }
570 })
571 } else {
572 var err = 'driver (' + self.driver.name + ') has not implemented a restore method'
573 self.emit('restore',err)
574 cb(err)
575 }
576}
577
578/**
579 * Flush the object stored by the storage driver
580 * @param [cb]
581 */
582ObjectManage.prototype.flush = function(cb){
583 var self = this
584 if('function' !== typeof cb){
585 cb = function(err){
586 if(err) throw err
587 }
588 }
589 if(null === self.handle){
590 throw new Error('cannot flush without a handle being set')
591 } else if('function' === typeof self.driver.flush){
592 self.driver.flush(self.handle,function(err){
593 if(err){
594 self.emit('flush',err)
595 cb(err)
596 } else {
597 self.emit('flush')
598 self.data = {}
599 cb()
600 }
601
602 })
603 } else {
604 var err = 'driver (' + self.driver.name + ') has not implemented a flush method'
605 self.emit('flush',err)
606 cb(err)
607 }
608}
609
610/**
611 * Reset data store
612 */
613ObjectManage.prototype.reset = function(){
614 this.data = {}
615}
616
617module.exports = ObjectManage
\No newline at end of file