| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | var crypto = require('crypto'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | function jsonDeepClone(target) { | 
					
						
							|  |  |  |   return JSON.parse( | 
					
						
							|  |  |  |     JSON.stringify(target) | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | function mergeObjects() { | 
					
						
							|  |  |  |   // arguments should be an array of objects. We
 | 
					
						
							|  |  |  |   // reverse it because the last argument to set
 | 
					
						
							|  |  |  |   // a value wins.
 | 
					
						
							|  |  |  |   var args = [].slice.call(arguments).reverse(); | 
					
						
							|  |  |  |   var len = args.length; | 
					
						
							|  |  |  |   if (len === 1) { | 
					
						
							|  |  |  |     return args[0]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // gather the set of keys from all arguments
 | 
					
						
							|  |  |  |   var keyLists = args.map(function (arg) { | 
					
						
							|  |  |  |     return Object.keys(arg); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var keys = Object.keys(keyLists.reduce(function (all, list) { | 
					
						
							|  |  |  |     list.forEach(function (k) { | 
					
						
							|  |  |  |       all[k] = true; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     return all; | 
					
						
							|  |  |  |   }, {})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // for each key
 | 
					
						
							|  |  |  |   return keys.reduce(function (target, k) { | 
					
						
							|  |  |  |     // find the first argument (because of the reverse() above) with the key set
 | 
					
						
							|  |  |  |     var values = []; | 
					
						
							|  |  |  |     var isObject = false; | 
					
						
							|  |  |  |     for (var i = 0; i < len; i++) { | 
					
						
							|  |  |  |       var v = args[i]; | 
					
						
							|  |  |  |       var vType = typeof v; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (vType === 'object') { | 
					
						
							|  |  |  |         if (!v) { | 
					
						
							|  |  |  |           // typeof null is object. null is the only falsey object. null represents
 | 
					
						
							|  |  |  |           // a delete or the end of our argument list;
 | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // we need to collect values until we get a non-object, so we can merge them
 | 
					
						
							|  |  |  |         values.push(v); | 
					
						
							|  |  |  |         isObject = true; | 
					
						
							|  |  |  |       } else if (!isObject) { | 
					
						
							|  |  |  |         if (vType === 'undefined') { | 
					
						
							|  |  |  |           // if the arg actually has the key set this is effectively a "delete"
 | 
					
						
							|  |  |  |           if (keyList[i].indexOf(k) != -1) { | 
					
						
							|  |  |  |             break; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           // otherwise we need to check the next argument's value, so we don't break the loop
 | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           values.push(v); | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         // a previous value was an object, this one isn't
 | 
					
						
							|  |  |  |         // That means we are done collecting values.
 | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (values.length > 0) { | 
					
						
							|  |  |  |       target[k] = mergeObjects.apply(null, values); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return target; | 
					
						
							|  |  |  |   }, {}); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function prepareZone(zone, options) { | 
					
						
							|  |  |  |   var opts = options || {}; | 
					
						
							|  |  |  |   var timestamp = opts.timestamp || Date.now(); | 
					
						
							|  |  |  |   if (!zone.name) { | 
					
						
							|  |  |  |     zone.name = zone.id; | 
					
						
							|  |  |  |     zone.id = null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (!zone.id) { | 
					
						
							|  |  |  |     zone.id = crypto.randomBytes(16).toString('hex'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (!zone.createdAt) { zone.createdAt = timestamp; } | 
					
						
							|  |  |  |   if (!zone.updatedAt || opts.isUpdate) { zone.updatedAt = timestamp; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // create a names set for the zone, keyed by record name mapped to
 | 
					
						
							|  |  |  |   // an object for the various records with that name, by type (A, MX, TXT, etc.)
 | 
					
						
							|  |  |  |   zone.records = zone.records || {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return zone; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | /* | 
					
						
							|  |  |  | init() should return an object with: { | 
					
						
							|  |  |  |   save: function -> undefined - changes to in memory representation should be persisted | 
					
						
							|  |  |  |   This could be considered the equivalent of committing a transaction to the database. | 
					
						
							|  |  |  |   primaryNameservers: { | 
					
						
							|  |  |  |     list: function -> list nameservers | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  |   zones: { | 
					
						
							|  |  |  |     list: function -> list zones, | 
					
						
							|  |  |  |     create: | 
					
						
							|  |  |  |     update: | 
					
						
							|  |  |  |     delete: | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  |   records: { | 
					
						
							|  |  |  |     list: function -> list records, | 
					
						
							|  |  |  |     create: | 
					
						
							|  |  |  |     update: | 
					
						
							|  |  |  |     delete: | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | All lists will be a deep copy of the data actually stored. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = function init (opts) { | 
					
						
							|  |  |  |   // opts = { filepath };
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var db = require(opts.filepath); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   var mtime = require('fs').statSync(opts.filepath).mtime.valueOf(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   //
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   // Migration from other formats
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   //
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   // Convert the primary nameservers from an array of strings to objects with names and IDs.
 | 
					
						
							|  |  |  |   // also switch to the 'peers' name, since we are really interested in the other FQDNs that
 | 
					
						
							|  |  |  |   // use the same data store and are kept in sync.
 | 
					
						
							|  |  |  |   var peerList = (!db.peers || Array.isArray(db.peers))? db.peers : Object.keys(db.peers).map(function (p) { | 
					
						
							|  |  |  |     return db.peers[p]; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   db.peers = [].concat(db.primaryNameservers, peerList).filter(function (p) { | 
					
						
							|  |  |  |     // filer out empty strings, undefined, etc.
 | 
					
						
							|  |  |  |     return !!p; | 
					
						
							|  |  |  |   }).map(function (ns) { | 
					
						
							|  |  |  |     var peer = ('string' === typeof ns)? ns : { name: ns }; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     if (!ns.id) { | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       peer.id = crypto.randomBytes(16).toString('hex'); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     return peer; | 
					
						
							|  |  |  |   }).reduce(function (peers, p) { | 
					
						
							|  |  |  |     peers[p.name] = p; | 
					
						
							|  |  |  |     return peers; | 
					
						
							|  |  |  |   }, {}); | 
					
						
							|  |  |  |   delete db.primaryNameservers; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // Convert domains to zones and ensure that they have proper IDs and timestamps
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   // Organize zones as a set of zone names
 | 
					
						
							|  |  |  |   var zoneList = (!db.zones || Array.isArray(db.zones))? db.zones : Object.keys(db.zones).map(function (z) { | 
					
						
							|  |  |  |     return db.zones[z]; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   db.zones = [].concat(db.domains, zoneList).filter(function (z) { | 
					
						
							|  |  |  |     // filer out empty strings, undefined, etc.
 | 
					
						
							|  |  |  |     return !!z; | 
					
						
							|  |  |  |   }).map(function (zone) { | 
					
						
							|  |  |  |     return prepareZone(zone, { timestamp: mtime }); | 
					
						
							|  |  |  |   }).reduce(function (zones, z) { | 
					
						
							|  |  |  |     zones[z.name] = z; | 
					
						
							|  |  |  |     return zones; | 
					
						
							|  |  |  |   }, {}); | 
					
						
							|  |  |  |   delete db.domains; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // NOTE: Records belong to zones, but they previously referred to them only by a
 | 
					
						
							|  |  |  |   // zone property. This may pose problems where the whole list of records is not easily
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   // filtered / kept in memory / indexed and/or retrieved by zone. Traditionally,
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   // records are stored "within a zone" in a zone file. We want to have the store API
 | 
					
						
							|  |  |  |   // behave more traditionally, even though some stores (like a SQL database
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   // table) might actually store the zone as a property of a record as we currently do.
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   // (This fits with the somewhat unexpected and confusing logic of wildcard records.)
 | 
					
						
							|  |  |  |   (db.records || []).forEach(function (record) { | 
					
						
							|  |  |  |     // make sure the record has an ID
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     if (!record.id) { | 
					
						
							|  |  |  |       record.id = crypto.randomBytes(16).toString('hex'); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // put it in it's zone - synthesize one if needed
 | 
					
						
							|  |  |  |     db.zones[record.zone] = db.zones[record.zone] || prepareZone({ name: record.zone }); | 
					
						
							|  |  |  |     var zone = db.zones[record.zone]; | 
					
						
							|  |  |  |     zone.records[record.name] = zone.records[record.name] || []; | 
					
						
							|  |  |  |     var recordsForName = zone.records[record.name]; | 
					
						
							|  |  |  |     recordsForName.push(record); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   delete db.records; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // Write the migrated data
 | 
					
						
							|  |  |  |   require('fs').writeFileSync(opts.filepath, JSON.stringify(db, null, 2)); | 
					
						
							|  |  |  |   //
 | 
					
						
							|  |  |  |   // End Migration
 | 
					
						
							|  |  |  |   //
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var save = function save (cb) { | 
					
						
							|  |  |  |     if (save._saving) { | 
					
						
							|  |  |  |       console.log('make pending'); | 
					
						
							|  |  |  |       save._pending.push(cb); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     save._saving = true; | 
					
						
							|  |  |  |     require('fs').writeFile(opts.filepath, JSON.stringify(db, null, 2), function (err) { | 
					
						
							|  |  |  |       console.log('done writing'); | 
					
						
							|  |  |  |       var pending = save._pending.splice(0); | 
					
						
							|  |  |  |       save._saving = false; | 
					
						
							|  |  |  |       cb(err); | 
					
						
							|  |  |  |       if (!pending.length) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       save(function (err) { | 
					
						
							|  |  |  |         console.log('double save'); | 
					
						
							|  |  |  |         pending.forEach(function (cb) { cb(err); }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   save._pending = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   function matchPredicate(predicate) { | 
					
						
							|  |  |  |     return function (toCheck) { | 
					
						
							|  |  |  |       // which items match the predicate?
 | 
					
						
							|  |  |  |       if (!toCheck) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // check all the keys in the predicate - only supporting exact match
 | 
					
						
							|  |  |  |       // of at least one listed option for all keys right now
 | 
					
						
							|  |  |  |       if (Object.keys(predicate || {}).some(function (k) { | 
					
						
							|  |  |  |         return [].concat(predicate[k]).indexOf(toCheck[k]) === -1; | 
					
						
							|  |  |  |       })) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // we have a match
 | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function matchZone(predicate) { | 
					
						
							|  |  |  |     var zonenames = !!predicate.name ? [].concat(predicate.name) : Object.keys(db.zones); | 
					
						
							|  |  |  |     var check = matchPredicate(predicate); | 
					
						
							|  |  |  |     // TODO: swap the filter() for a functional style "loop" recursive function
 | 
					
						
							|  |  |  |     // that lets us return early if we have a limit, etc.
 | 
					
						
							|  |  |  |     var found = zonenames.filter(function (zonename) { | 
					
						
							|  |  |  |       /* | 
					
						
							|  |  |  |       if (predicate.id && predicate.id !== z.id) { return false; } | 
					
						
							|  |  |  |       if (predicate.name && predicate.name !== z.name) { return false; } | 
					
						
							|  |  |  |       */ | 
					
						
							|  |  |  |       return check(db.zones[zonename]); | 
					
						
							|  |  |  |     }).map(function (zonename) { | 
					
						
							|  |  |  |       return db.zones[zonename]; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return found; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   var dbApi = { | 
					
						
							|  |  |  |     save: function () { | 
					
						
							|  |  |  |       // hide _pending and _saving from callers
 | 
					
						
							|  |  |  |       var args = [].slice.call(arguments); | 
					
						
							|  |  |  |       return save.apply(null, args); | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     // peers really isn't editable - it's literally the list of FQDN's
 | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     // that this database is replicated to in a multi-master fashion.
 | 
					
						
							|  |  |  |     //
 | 
					
						
							|  |  |  |     // However, lib/store/index.js does plenty to update these records in support
 | 
					
						
							|  |  |  |     // of the SOA records that are built from them (as does this file in the "migration"
 | 
					
						
							|  |  |  |     // section). I'm toying with the idea of not storing them seperately or creating the
 | 
					
						
							|  |  |  |     // SOA records somewhat immediately.
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     peers: function listPeers(cb) { | 
					
						
							|  |  |  |       // Most data stores are going to have an asynchronous storage API. If we need
 | 
					
						
							|  |  |  |       // synchronous access to the data it is going to have to be cached. If it is
 | 
					
						
							|  |  |  |       // cached, there is still the issue the cache getting out of sync (a legitimate
 | 
					
						
							|  |  |  |       // issue anyway). If we explicitly make all of these operations async then we
 | 
					
						
							|  |  |  |       // have greater flexibility for store implmentations to address these issues.
 | 
					
						
							|  |  |  |       return setImmediate(cb, null, jsonDeepClone(db.peers)); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     zones: { | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       /* | 
					
						
							|  |  |  |       I'm fairly certan that zone names must be unique and therefore are legitimately | 
					
						
							|  |  |  |       IDs within the zones namespace. This is similarly true of record names within a zone. | 
					
						
							|  |  |  |       I'm not certain that having a distinct ID adds value and it may add confusion / complexity. | 
					
						
							|  |  |  |        */ | 
					
						
							|  |  |  |       // NOTE: `opts` exists so we can add options - like properties to read - easily in the future
 | 
					
						
							|  |  |  |       // without modifying the function signature
 | 
					
						
							|  |  |  |       list: function listZones(predicate, opts, cb) { | 
					
						
							|  |  |  |         // TODO: consider whether we should just return the zone names
 | 
					
						
							|  |  |  |         var found = jsonDeepClone(matchZone(predicate)).map(function (z) { | 
					
						
							|  |  |  |           // This is fairly inefficient!! Consider alternative storage
 | 
					
						
							|  |  |  |           // that does not require deleting the records like this.
 | 
					
						
							|  |  |  |           delete z.records; | 
					
						
							|  |  |  |           return z; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         return setImmediate(cb, null, found); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |       }, | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       // // NOTE: I'm not sure we need a distinct 'find()' operation in the API
 | 
					
						
							|  |  |  |       // // unless we are going to limit the output of the
 | 
					
						
							|  |  |  |       // // 'list()' operation in some incompatible way.
 | 
					
						
							|  |  |  |       // // NOTE: `opts` exists so we can add options - like properties to read - easily in the future
 | 
					
						
							|  |  |  |       // // without modifying the function signature
 | 
					
						
							|  |  |  |       // find: function getZone(predicate, opts, cb) {
 | 
					
						
							|  |  |  |       //   if (!predicate.name || predicate.id) {
 | 
					
						
							|  |  |  |       //     return setImmediate(cb, new Error('Finding a zone requires a `name` or `id`'));
 | 
					
						
							|  |  |  |       //   }
 | 
					
						
							|  |  |  |       //   // TODO: implement a limit / short circuit and possibly offset
 | 
					
						
							|  |  |  |       //   // to allow for paging of zone data.
 | 
					
						
							|  |  |  |       //   var found = matchZone(predicate);
 | 
					
						
							|  |  |  |       //   if (!found[0]) {
 | 
					
						
							|  |  |  |       //     // TODO: make error message more specific?
 | 
					
						
							|  |  |  |       //     return setImmediate(cb, new Error('Zone not found'));
 | 
					
						
							|  |  |  |       //   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       //   var z = jsonDeepClone(found[0]);
 | 
					
						
							|  |  |  |       //   delete z.records;
 | 
					
						
							|  |  |  |       //   return setImmediate(cb, null, z);
 | 
					
						
							|  |  |  |       // },
 | 
					
						
							|  |  |  |       create: function createZone(zone, cb) { | 
					
						
							|  |  |  |         // We'll need a lock mechanism of some sort that works
 | 
					
						
							|  |  |  |         // for simultaneous requests and multiple processes.
 | 
					
						
							|  |  |  |         matchZone({ name: zone.name }, function (err, matched) { | 
					
						
							|  |  |  |           if (err) { | 
					
						
							|  |  |  |             return setImmediate(cb, err); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           var found = matched[0]; | 
					
						
							|  |  |  |           if (found) { | 
					
						
							|  |  |  |             return setImmediate(cb, new Error('Zone ' + zone.name + ' already exists')); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |            | 
					
						
							|  |  |  |           db.zones[zone.name] = prepareZone(zone); | 
					
						
							|  |  |  |           return setImmediate(function () { | 
					
						
							|  |  |  |             cb(null, jsonDeepClone(db.zones[zone.name])); | 
					
						
							|  |  |  |             // release lock
 | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       update: function updateZone(zone, cb) { | 
					
						
							|  |  |  |         // We'll need a lock mechanism of some sort that works
 | 
					
						
							|  |  |  |         // for simultaneous requests and multiple processes.
 | 
					
						
							|  |  |  |         matchZone({ name: zone.name }, function (err, matched) { | 
					
						
							|  |  |  |           if (err) { | 
					
						
							|  |  |  |             return setImmediate(cb, err); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           var found = matched[0]; | 
					
						
							|  |  |  |           if (!found) { | 
					
						
							|  |  |  |             return setImmediate(cb, new Error('Zone not found')); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           // make sure we are not writing records through this interface
 | 
					
						
							|  |  |  |           delete zone.records; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           var combined = mergeObjects(found, zone); | 
					
						
							|  |  |  |           db.zones[zone.name] = prepareZone(combined, { isUpdate: true }); | 
					
						
							|  |  |  |           return setImmediate(function () { | 
					
						
							|  |  |  |             cb(null, jsonDeepClone(db.zones[zone.name])); | 
					
						
							|  |  |  |             // release lock
 | 
					
						
							|  |  |  |           }); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |         }); | 
					
						
							|  |  |  |       }, | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       delete: function(zone, cb) { | 
					
						
							|  |  |  |         // We'll need a lock mechanism of some sort that works
 | 
					
						
							|  |  |  |         // for simultaneous requests and multiple processes.
 | 
					
						
							|  |  |  |         matchZone({ name: zone.name }, function (err, matched) { | 
					
						
							|  |  |  |           if (err) { | 
					
						
							|  |  |  |             return setImmediate(cb, err); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           var found = matched[0]; | 
					
						
							|  |  |  |           if (!found) { | 
					
						
							|  |  |  |             return setImmediate(cb, new Error('Zone not found')); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           delete db.zones[zone.name]; | 
					
						
							|  |  |  |           return setImmediate(function () { | 
					
						
							|  |  |  |             cb(); | 
					
						
							|  |  |  |             // release lock
 | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     records: { | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       list: function listRecords(rPredicate, cb) { | 
					
						
							|  |  |  |         var recordNames = [].concat(rPredicate.name); | 
					
						
							|  |  |  |         var check = matchPredicate(rPredicate); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var found = matchZone({ name: rPredicate.zone }).reduce(function (records, zone) { | 
					
						
							|  |  |  |           // get the records from the zone that match the record predicate
 | 
					
						
							|  |  |  |           var zFound = recordNames.filter(function (name) { | 
					
						
							|  |  |  |             return !!zone.records[name]; | 
					
						
							|  |  |  |           }).map(function (name) { | 
					
						
							|  |  |  |             return zone.records[name].filter(check); | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |           return records.concat(zFound); | 
					
						
							|  |  |  |         }, []); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return setImmediate(cb, null, jsonDeepClone(found)); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |       }, | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       // find: function getRecord(rPredicate, cb) {
 | 
					
						
							|  |  |  |       //   var recordNames = [].concat(rPredicate.name);
 | 
					
						
							|  |  |  |       //   var check = matchPredicate(rPredicate);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       //   // TODO: swap the `filter()` and `map()` for a functional style "loop"
 | 
					
						
							|  |  |  |       //   // recursive function that lets us return early if we have a limit, etc.
 | 
					
						
							|  |  |  |       //   var found = matchZone({ name: rPredicate.zone }).reduce(function (records, zone) {
 | 
					
						
							|  |  |  |       //     // get the records from the zone that match the record predicate
 | 
					
						
							|  |  |  |       //     var zFound = recordNames.filter(function (name) {
 | 
					
						
							|  |  |  |       //       return !!zone.records[name];
 | 
					
						
							|  |  |  |       //     }).map(function (name) {
 | 
					
						
							|  |  |  |       //       return zone.records[name].filter(check);
 | 
					
						
							|  |  |  |       //     });
 | 
					
						
							|  |  |  |       //     return records.concat(zFound);
 | 
					
						
							|  |  |  |       //   }, []);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       //   return setImmediate(cb, null, jsonDeepClone(found[0]));
 | 
					
						
							|  |  |  |       // },
 | 
					
						
							|  |  |  |       create: function(record, cb) { | 
					
						
							|  |  |  |         var zone = matchZone({ name: record.zone })[0]; | 
					
						
							|  |  |  |         if (!zone) { | 
					
						
							|  |  |  |           return setImmediate(cb, new Error('Unble to find zone ' + record.zone + ' to create record')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var records = zone.records[record.name] = zone.records[record.name] || []; | 
					
						
							|  |  |  |         var check = matchPredicate(record); | 
					
						
							|  |  |  |         if (records.filter(check)[0]) { | 
					
						
							|  |  |  |           return setImmediate(cb, new Error('Exact record already exists in zone ' + record.zone )); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return setImmediate(cb, null, jsonDeepClone(found)); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |       }, | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       update: function(record, cb) {}, | 
					
						
							|  |  |  |       delete: function(record, cb) {} | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return dbApi; | 
					
						
							|  |  |  | }; |