| 
									
										
										
										
											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
										 |  |  | /* | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  | `init()` should return a `lock(forOps)` function, where `forOps` describes the portions | 
					
						
							|  |  |  | of the database that we need to obtain a lock for (so we can write to them). If `forOps` | 
					
						
							|  |  |  | is underfined, we only need to read the currently valid data. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `lock(forOps)` 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. | 
					
						
							|  |  |  |     This will release any write lock obtained. `save()` will return an error if no write | 
					
						
							|  |  |  |     lock was obtained OR writes are made to locations other than were locked., | 
					
						
							|  |  |  |   discard: function -> undefined - changes to in memory representation should be discarded. | 
					
						
							|  |  |  |     This could be considered the equivalent of cancelling a transaction to the database. | 
					
						
							|  |  |  |     This will release any write lock obtained., | 
					
						
							|  |  |  |   peers: { | 
					
						
							|  |  |  |     list: function -> list FQDNs that we expec to be in sync with this server | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   }, | 
					
						
							|  |  |  |   zones: { | 
					
						
							|  |  |  |     list: function -> list zones, | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     write: | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     delete: | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  |   records: { | 
					
						
							|  |  |  |     list: function -> list records, | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     write: | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     delete: | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | All lists will be a deep copy of the data actually stored. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = function init (opts) { | 
					
						
							|  |  |  |   // opts = { filepath };
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   var fsDb = 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.
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   var peerList = (!fsDb.peers || Array.isArray(fsDb.peers))? fsDb.peers : Object.keys(fsDb.peers).map(function (p) { | 
					
						
							|  |  |  |     return fsDb.peers[p]; | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   fsDb.peers = [].concat(fsDb.primaryNameservers, peerList).filter(function (p) { | 
					
						
							|  |  |  |     // filter out empty strings, undefined, etc.
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     return !!p; | 
					
						
							|  |  |  |   }).map(function (ns) { | 
					
						
							|  |  |  |     var peer = ('string' === typeof ns)? ns : { name: ns }; | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     if (!peer.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; | 
					
						
							|  |  |  |   }, {}); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   delete fsDb.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
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   var zoneList = (!fsDb.zones || Array.isArray(fsDb.zones))? fsDb.zones : Object.keys(fsDb.zones).map(function (z) { | 
					
						
							|  |  |  |     return fsDb.zones[z]; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   fsDb.zones = [].concat(fsDb.domains, zoneList).filter(function (z) { | 
					
						
							|  |  |  |     // filter out empty strings, undefined, etc.
 | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     return !!z; | 
					
						
							|  |  |  |   }).map(function (zone) { | 
					
						
							|  |  |  |     return prepareZone(zone, { timestamp: mtime }); | 
					
						
							|  |  |  |   }).reduce(function (zones, z) { | 
					
						
							|  |  |  |     zones[z.name] = z; | 
					
						
							|  |  |  |     return zones; | 
					
						
							|  |  |  |   }, {}); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   delete fsDb.domains; | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // 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.)
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   (fsDb.records || []).forEach(function (record) { | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     // 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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     // Put it in it's zone - synthesize one if needed
 | 
					
						
							|  |  |  |     fsDb.zones[record.zone] = fsDb.zones[record.zone] || prepareZone({ name: record.zone }); | 
					
						
							|  |  |  |     var zone = fsDb.zones[record.zone]; | 
					
						
							|  |  |  |     // Keep in mind that each name may have multiple records (whether or not they are
 | 
					
						
							|  |  |  |     // of different types, classes, etc.), but each record must have a unique ID.
 | 
					
						
							|  |  |  |     zone.records[record.name] = zone.records[record.name] || {}; | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |     var recordsForName = zone.records[record.name]; | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     recordsForName[record.id] = record; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   delete fsDb.records; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // Write the migrated data
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   require('fs').writeFileSync(opts.filepath, JSON.stringify(fsDb, null, 2)); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |   //
 | 
					
						
							|  |  |  |   // End Migration
 | 
					
						
							|  |  |  |   //
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |   return function lock(forOps) { | 
					
						
							|  |  |  |     /* | 
					
						
							|  |  |  |       forOps : { | 
					
						
							|  |  |  |         write: { | 
					
						
							|  |  |  |           zone: string - required - a zone name, | 
					
						
							|  |  |  |           names: [string] - optional - a list of record names that may be modified. May be 0 length, | 
					
						
							|  |  |  |           records: [string] - optional - a list of record IDs that may be modified. May be 0 length (default) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |       1. You can't get a lock for a whole zone without first releasing any locks for names and records | 
					
						
							|  |  |  |         within the zone. A whole zone lock will block | 
					
						
							|  |  |  |       2. You can't get a lock for a name within a zone without first releasing any locks for records | 
					
						
							|  |  |  |         within that name and zone. | 
					
						
							|  |  |  |       3. Locks for a specific record do not block new locks with the same zone, name, but a different | 
					
						
							|  |  |  |         record ID. | 
					
						
							|  |  |  |       4. Creating a new zone, name, or record requires obtaining a lock for it's key (name or ID), even | 
					
						
							|  |  |  |         though it does not exist yet. This prevents race conditions where 2 requests (or processes) attempt | 
					
						
							|  |  |  |         to create the same resource at the same time. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       Note: The UI probably needs to know if it is trying to write based on an outdated copy of data. Such | 
					
						
							|  |  |  |       writes should be detected and fail loudly. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       locks probably involve lockfiles on the filesystem (with watches) so that writes and locks can be | 
					
						
							|  |  |  |       communicated easily across processes. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     var db = mergeObjects(fsDb); | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |     var save = function save (cb) { | 
					
						
							|  |  |  |       if (save._saving) { | 
					
						
							|  |  |  |         console.log('make pending'); | 
					
						
							|  |  |  |         save._pending.push(cb); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       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); }); | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     }; | 
					
						
							|  |  |  |     save._pending = []; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     function matchPredicate(predicate) { | 
					
						
							|  |  |  |       return function (toCheck) { | 
					
						
							|  |  |  |         // which items match the predicate?
 | 
					
						
							|  |  |  |         if (!toCheck) { | 
					
						
							|  |  |  |           return false; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |         // 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; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |         // we have a match
 | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     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]; | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |       return found; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // NOTE: `opts` exists so we can add options - like properties to read - easily in the future
 | 
					
						
							|  |  |  |     // without modifying the function signature
 | 
					
						
							|  |  |  |     function listZones(predicate, opts, cb) { | 
					
						
							|  |  |  |       var found = jsonDeepClone(matchZone(predicate)) | 
					
						
							|  |  |  |       return setImmediate(cb, null, found); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function writeZone(zone, cb) { | 
					
						
							|  |  |  |       matchZone({ name: zone.name }, function (err, matched) { | 
					
						
							|  |  |  |         if (err) { | 
					
						
							|  |  |  |           return setImmediate(cb, err); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var found = matched[0]; | 
					
						
							|  |  |  |         var isUpdate = !!found; | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |         var combined = mergeObjects((found || {}), zone); | 
					
						
							|  |  |  |         db.zones[zone.name] = prepareZone(combined, { isUpdate: isUpdate }); | 
					
						
							|  |  |  |         return setImmediate(function () { | 
					
						
							|  |  |  |           cb(null, jsonDeepClone(db.zones[zone.name])); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     function deleteZone(zone, cb) { | 
					
						
							|  |  |  |       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(); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     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 Object.keys(zone.records[name]).map(function (id) { | 
					
						
							|  |  |  |             return zone.records[name][id]; | 
					
						
							|  |  |  |           }).filter(check); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |         return records.concat(zFound); | 
					
						
							|  |  |  |       }, []); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |       return setImmediate(cb, null, jsonDeepClone(found)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function modifyRecords (record, options, cb) { | 
					
						
							|  |  |  |       var opts = options || {}; | 
					
						
							|  |  |  |       var isDelete = !!opts.isDelete; | 
					
						
							|  |  |  |       if (!record.zone) { | 
					
						
							|  |  |  |         return setImmediate(cb, new Error('No zone specified for record')); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (!record.name) { | 
					
						
							|  |  |  |         return setImmediate(cb, new Error('No name specified for record')); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (isDelete && !record.id) { | 
					
						
							|  |  |  |         return setImmediate(cb, new Error('No id specified to delete record')); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |       var zone = matchZone({ name: record.zone })[0]; | 
					
						
							|  |  |  |       if (!zone) { | 
					
						
							|  |  |  |         return setImmediate(cb, new Error('Unble to find zone ' + record.zone + ' for record')); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       var isUpdate = (record.id && !isDelete); | 
					
						
							|  |  |  |       if (!isUpdate) { | 
					
						
							|  |  |  |         record.id = crypto.randomBytes(16).toString('hex'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |       var recordsForName = zone.records[record.name] = zone.records[record.name] || {}; | 
					
						
							|  |  |  |       var found = recordsForName[record.id]; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |       if ((isUpdate || isDelete) && !found) { | 
					
						
							|  |  |  |         return setImmediate(cb, new Error('Unable to find record with ID: ' + record.id));   | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |       if (!isDelete) { | 
					
						
							|  |  |  |         recordsForName[record.id] = (mergeObjects((found || {}), record)); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |       var zoneUpdate = { | 
					
						
							|  |  |  |         name: record.name, | 
					
						
							|  |  |  |         records: {} | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       zoneUpdate.records[record.name] = keep; | 
					
						
							|  |  |  |       return writeZone(zoneUpdate, function (err) { | 
					
						
							|  |  |  |         if (err) { | 
					
						
							|  |  |  |           return cb(err); | 
					
						
							| 
									
										
										
										
											2018-11-24 22:36:04 -07:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |         return cb( | 
					
						
							|  |  |  |           null, | 
					
						
							|  |  |  |           isDelete ? null : jsonDeepClone(recordsForName[record.id]) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-25 01:29:16 -07:00
										 |  |  |     function writeRecord(record, cb) { | 
					
						
							|  |  |  |       modifyRecords(record, null, cb); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function deleteRecord(record, cb) { | 
					
						
							|  |  |  |       modifyRecords(record, { isDelete: true }, cb); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     var dbApi = { | 
					
						
							|  |  |  |       save: function () { | 
					
						
							|  |  |  |         // hide _pending and _saving from callers
 | 
					
						
							|  |  |  |         var args = [].slice.call(arguments); | 
					
						
							|  |  |  |         return save.apply(null, args); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       // peers really isn't editable - it's literally the list of FQDN's
 | 
					
						
							|  |  |  |       // 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.
 | 
					
						
							|  |  |  |       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)); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       zones: { | 
					
						
							|  |  |  |         list: listZones, | 
					
						
							|  |  |  |         write: writeZone, | 
					
						
							|  |  |  |         delete: deleteZone | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       records: { | 
					
						
							|  |  |  |         list: listRecords, | 
					
						
							|  |  |  |         write: writeRecord, | 
					
						
							|  |  |  |         delete: deleteRecord | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return dbApi; | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2018-11-21 10:53:54 -07:00
										 |  |  | }; |