2017-11-05 16:16:27 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
module.exports.create = function (opts) {
|
|
|
|
// opts = { filepath };
|
|
|
|
var engine = { db: null };
|
|
|
|
|
2018-01-29 21:54:03 +00:00
|
|
|
function notDeleted(r) {
|
|
|
|
return !r.revokedAt && !r.deletedAt;
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:16:27 +00:00
|
|
|
var db = require(opts.filepath);
|
2018-01-26 08:42:24 +00:00
|
|
|
var stat = require('fs').statSync(opts.filepath);
|
2018-01-21 08:47:37 +00:00
|
|
|
var crypto = require('crypto');
|
2018-01-29 22:17:55 +00:00
|
|
|
//
|
|
|
|
// Manual Migration
|
|
|
|
//
|
|
|
|
db.primaryNameservers.forEach(function (ns, i, arr) {
|
|
|
|
if ('string' === typeof ns) {
|
2018-01-29 22:54:56 +00:00
|
|
|
ns = { name: ns };
|
|
|
|
arr[i] = ns;
|
2018-01-29 22:17:55 +00:00
|
|
|
}
|
2018-01-21 08:47:37 +00:00
|
|
|
if (!ns.id) {
|
|
|
|
ns.id = crypto.randomBytes(16).toString('hex');
|
|
|
|
}
|
|
|
|
});
|
2018-01-29 22:17:55 +00:00
|
|
|
db.zones = db.zones || [];
|
|
|
|
if (db.domains) {
|
|
|
|
db.zones = db.zones.concat(db.domains);
|
|
|
|
}
|
2018-01-21 08:47:37 +00:00
|
|
|
db.zones.forEach(function (zone) {
|
|
|
|
if (!zone.name) {
|
|
|
|
zone.name = zone.id;
|
|
|
|
zone.id = null;
|
|
|
|
}
|
|
|
|
if (!zone.id) {
|
|
|
|
zone.id = crypto.randomBytes(16).toString('hex');
|
|
|
|
}
|
2018-01-26 08:42:24 +00:00
|
|
|
if (!zone.createdAt) { zone.createdAt = stat.mtime.valueOf(); }
|
|
|
|
if (!zone.updatedAt) { zone.updatedAt = stat.mtime.valueOf(); }
|
2018-01-21 08:47:37 +00:00
|
|
|
});
|
|
|
|
db.records.forEach(function (record) {
|
|
|
|
if (!record.id) {
|
|
|
|
record.id = crypto.randomBytes(16).toString('hex');
|
|
|
|
}
|
|
|
|
});
|
2018-01-29 22:17:55 +00:00
|
|
|
require('fs').writeFileSync(opts.filepath, JSON.stringify(db, null, 2));
|
|
|
|
//
|
|
|
|
// End Migration
|
|
|
|
//
|
|
|
|
|
2018-01-23 22:21:20 +00:00
|
|
|
db.save = function (cb) {
|
|
|
|
if (db.save._saving) {
|
|
|
|
console.log('make pending');
|
|
|
|
db.save._pending.push(cb);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
db.save._saving = true;
|
|
|
|
require('fs').writeFile(opts.filepath, JSON.stringify(db, null, 2), function (err) {
|
|
|
|
console.log('done writing');
|
|
|
|
var pending = db.save._pending.splice(0);
|
|
|
|
db.save._saving = false;
|
|
|
|
cb(err);
|
|
|
|
if (!pending.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
db.save(function (err) {
|
|
|
|
console.log('double save');
|
|
|
|
pending.forEach(function (cb) { cb(err); });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
db.save._pending = [];
|
2017-11-05 16:16:27 +00:00
|
|
|
|
|
|
|
engine.primaryNameservers = db.primaryNameservers;
|
2018-01-10 21:54:08 +00:00
|
|
|
engine.peers = {
|
|
|
|
all: function (cb) {
|
2018-01-30 19:03:25 +00:00
|
|
|
var dns = require('dns');
|
|
|
|
var count = db.primaryNameservers.length;
|
|
|
|
function gotRecord() {
|
|
|
|
count -= 1;
|
|
|
|
if (!count) {
|
|
|
|
cb(null, db.primaryNameservers);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function getRecord(ns) {
|
|
|
|
dns.resolve4(ns.name, function (err, addresses) {
|
|
|
|
console.log('ns addresses:');
|
|
|
|
console.log(addresses);
|
|
|
|
if (err) { console.error(err); gotRecord(); return; }
|
|
|
|
ns.type = 'A';
|
|
|
|
ns.address = addresses[0];
|
|
|
|
gotRecord();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
db.primaryNameservers.forEach(getRecord);
|
2018-01-10 21:54:08 +00:00
|
|
|
}
|
|
|
|
};
|
2018-01-10 08:14:05 +00:00
|
|
|
engine.zones = {
|
2018-01-31 05:03:05 +00:00
|
|
|
_immutableKeys: [ 'id', 'name', 'primary', 'serial', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ]
|
|
|
|
, _mutableKeys: [ 'admin', 'expiration', 'minimum', 'refresh', 'retry', 'ttl', 'vanity' ]
|
|
|
|
, _dateToSerial: function (date) {
|
|
|
|
// conventionally the format is YYYYMMDDxx,
|
|
|
|
// but since it's an integer and I don't want to keep track of incrementing xx,
|
|
|
|
// epoch in seconds will do
|
|
|
|
return parseInt(Math.round(date/1000).toString().slice(-10), 10);
|
|
|
|
}
|
2018-02-01 02:09:26 +00:00
|
|
|
, _toSoa: function (domain) {
|
|
|
|
var nameservers = domain.vanityNs || engine.primaryNameservers.map(function (n) { return n.name; });
|
|
|
|
|
|
|
|
var index = Math.floor(Math.random() * nameservers.length) % nameservers.length;
|
|
|
|
var nameserver = nameservers[index];
|
|
|
|
return {
|
|
|
|
id: domain.id
|
|
|
|
, name: domain.name
|
|
|
|
, typeName: 'SOA'
|
|
|
|
, className: 'IN'
|
|
|
|
, ttl: domain.ttl || 60
|
|
|
|
|
|
|
|
// nameserver -- select an NS at random if they're all in sync
|
|
|
|
, primary: nameserver
|
|
|
|
, name_server: nameserver
|
|
|
|
|
|
|
|
// admin -- email address or domain for admin
|
|
|
|
, admin: domain.admin || ('admin.' + domain.name)
|
|
|
|
, email_addr: domain.admin || ('admin.' + domain.name)
|
|
|
|
|
|
|
|
// serial -- the version, for cache-busting of secondary nameservers. suggested format: YYYYMMDDnn
|
|
|
|
, serial: domain.serial || engine.zones._dateToSerial(domain.updatedAt || domain.createdAt || Date.now())
|
|
|
|
, sn: domain.serial || engine.zones._dateToSerial(domain.updatedAt || domain.createdAt || Date.now())
|
|
|
|
|
|
|
|
// refresh -- only used when nameservers following the DNS NOTIFY spec talk
|
|
|
|
, refresh: domain.refresh || 1800
|
|
|
|
, ref: domain.refresh || 1800
|
|
|
|
|
|
|
|
// retry -- only used when nameservers following the DNS NOTIFY spec talk
|
|
|
|
, retry: domain.retry || 600
|
|
|
|
, ret: domain.retry || 600
|
|
|
|
|
|
|
|
// expiration -- how long other nameservers should continue when the primary goes down
|
|
|
|
, expiration: domain.expiration || 2419200 // 4 weeks
|
|
|
|
, ex: domain.expiration || 2419200 // 4 weeks
|
|
|
|
|
|
|
|
// minimum -- how long to cache a non-existent domain (also the default ttl for BIND)
|
|
|
|
, minimum: domain.minimum || 5
|
|
|
|
, nx: domain.minimum || 5
|
|
|
|
};
|
|
|
|
}
|
2018-01-31 05:03:05 +00:00
|
|
|
, all: function (cb) {
|
2018-01-10 08:14:05 +00:00
|
|
|
process.nextTick(function () {
|
2018-01-29 21:54:03 +00:00
|
|
|
cb(null, db.zones.slice(0).filter(notDeleted));
|
2018-01-10 08:14:05 +00:00
|
|
|
});
|
|
|
|
}
|
2018-01-18 00:01:44 +00:00
|
|
|
, get: function (queries, cb) {
|
|
|
|
if (!Array.isArray(queries)) {
|
|
|
|
queries = queries.names.map(function (n) {
|
|
|
|
return { name: n };
|
|
|
|
});
|
|
|
|
}
|
2018-01-21 08:47:37 +00:00
|
|
|
var myDomains = db.zones.filter(function (d) {
|
2018-01-18 00:01:44 +00:00
|
|
|
return queries.some(function (q) {
|
2018-01-29 21:54:03 +00:00
|
|
|
return (d.name.toLowerCase() === q.name) && notDeleted(d);
|
2018-01-18 00:01:44 +00:00
|
|
|
});
|
2018-01-10 08:14:05 +00:00
|
|
|
});
|
|
|
|
process.nextTick(function () {
|
|
|
|
cb(null, myDomains);
|
|
|
|
});
|
|
|
|
}
|
2018-01-26 08:42:24 +00:00
|
|
|
, touch: function (zone, cb) {
|
|
|
|
var existing;
|
|
|
|
db.zones.some(function (z) {
|
|
|
|
if (z.id && zone.id === z.id) { existing = z; return true; }
|
|
|
|
if (z.name && zone.name === z.name) { existing = z; return true; }
|
|
|
|
});
|
|
|
|
if (!existing) {
|
|
|
|
cb(null, null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
existing.updatedAt = new Date().valueOf(); // toISOString();
|
|
|
|
console.log('touch saving...');
|
|
|
|
db.save(function (err) {
|
|
|
|
cb(err, !err && existing || null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
, save: function (zone, cb) {
|
|
|
|
if (zone.id) {
|
|
|
|
console.log('update zone!');
|
|
|
|
engine.zones.update(zone, cb);
|
|
|
|
} else {
|
|
|
|
engine.zones.create(zone, cb);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
, update: function (zone, cb) {
|
|
|
|
var existing;
|
|
|
|
var dirty;
|
|
|
|
|
|
|
|
db.zones.some(function (z) {
|
|
|
|
if (z.id === zone.id) {
|
|
|
|
existing = z;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
console.log('no existing zone');
|
|
|
|
cb(new Error("zone for '" + zone.id + "' does not exist"), null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('found existing zone');
|
|
|
|
console.log(existing);
|
|
|
|
console.log(zone);
|
|
|
|
Object.keys(zone).forEach(function (key) {
|
2018-01-31 05:03:05 +00:00
|
|
|
if (-1 !== engine.zones._immutableKeys.indexOf(key)) { return; }
|
2018-01-26 08:42:24 +00:00
|
|
|
if (existing[key] !== zone[key]) {
|
|
|
|
dirty = true;
|
|
|
|
console.log('existing key', key, existing[key], zone[key]);
|
|
|
|
existing[key] = zone[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
zone.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000);
|
|
|
|
if (dirty) {
|
|
|
|
zone.changedAt = zone.updatedAt;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('saving...');
|
|
|
|
db.save(function (err) {
|
|
|
|
cb(err, !err && existing || null);
|
|
|
|
});
|
|
|
|
}
|
2018-01-31 05:03:05 +00:00
|
|
|
, create: function (zone, cb) {
|
|
|
|
var newZone = { id: crypto.randomBytes(16).toString('hex') };
|
|
|
|
var existing;
|
|
|
|
var nss = [];
|
|
|
|
|
|
|
|
zone.name = (zone.name||'').toLowerCase();
|
|
|
|
db.zones.some(function (z) {
|
|
|
|
if (z.name === zone.name) {
|
|
|
|
existing = z;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
cb(new Error("tried to create new zone, but '" + existing.name + "' already exists"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
newZone.name = zone.name;
|
|
|
|
newZone.createdAt = Date.now();
|
|
|
|
newZone.updatedAt = newZone.createdAt;
|
|
|
|
|
|
|
|
Object.keys(zone).forEach(function (key) {
|
|
|
|
//if (-1 !== engine.zones._immutableKeys.indexOf(key)) { return; }
|
|
|
|
if (-1 === engine.zones._mutableKeys.indexOf(key)) { return; }
|
|
|
|
newZone[key] = zone[key];
|
|
|
|
});
|
|
|
|
|
|
|
|
// TODO create NS and A records for normal and vanity nameservers
|
|
|
|
if (zone.vanity) {
|
|
|
|
newZone.vanity = true;
|
|
|
|
} else {
|
|
|
|
newZone.vanity = false;
|
|
|
|
}
|
|
|
|
db.primaryNameservers.forEach(function (ns, i) {
|
|
|
|
var nsx = 'ns' + (i + 1);
|
|
|
|
var nsZone;
|
|
|
|
var ttl = 43200; // 12h // TODO pick a well-reasoned number
|
|
|
|
var now = Date.now();
|
|
|
|
|
|
|
|
if (zone.vanity) {
|
|
|
|
nsZone = nsx + '.' + newZone.name;
|
|
|
|
} else {
|
|
|
|
nsZone = ns.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
// NS example.com ns1.example.com 43200
|
|
|
|
nss.push({
|
|
|
|
id: crypto.randomBytes(16).toString('hex')
|
|
|
|
, createdAt: Date.now()
|
|
|
|
, updatedAt: Date.now()
|
|
|
|
, changedAt: Date.now()
|
|
|
|
, zone: newZone.name
|
|
|
|
, soa: true
|
|
|
|
, type: 'NS'
|
|
|
|
, data: nsZone
|
|
|
|
, name: newZone.name
|
|
|
|
, ttl: ttl
|
|
|
|
});
|
|
|
|
// A ns1.example.com 127.0.0.1 43200
|
|
|
|
nss.push({
|
|
|
|
id: crypto.randomBytes(16).toString('hex')
|
|
|
|
, createdAt: now
|
|
|
|
, updatedAt: now
|
|
|
|
, changedAt: now
|
|
|
|
, zone: newZone.name
|
|
|
|
, soa: true
|
|
|
|
, type: ns.type
|
|
|
|
, name: nsZone
|
|
|
|
, address: ns.address
|
|
|
|
, ttl: 43200 // 12h // TODO pick a good number
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
db.zones.push(newZone);
|
|
|
|
nss.forEach(function (ns) {
|
|
|
|
db.records.push(ns);
|
|
|
|
});
|
|
|
|
|
2018-02-01 02:09:26 +00:00
|
|
|
console.log('[zone] [create] saving...');
|
2018-01-31 05:03:05 +00:00
|
|
|
db.save(function (err) {
|
|
|
|
cb(err, !err && newZone || null);
|
|
|
|
});
|
|
|
|
}
|
2018-02-01 02:09:26 +00:00
|
|
|
, destroy: function (zoneId, cb) {
|
|
|
|
var zone;
|
|
|
|
var records;
|
2018-02-01 02:19:11 +00:00
|
|
|
var now = Date.now();
|
|
|
|
|
2018-02-01 02:09:26 +00:00
|
|
|
db.zones.filter(notDeleted).some(function (z) {
|
|
|
|
if (zoneId === z.id) {
|
|
|
|
zone = z;
|
2018-02-01 02:19:11 +00:00
|
|
|
z.deletedAt = now;
|
2018-02-01 02:09:26 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!zone) {
|
|
|
|
process.nextTick(function () {
|
|
|
|
cb(null, null);
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
records = [];
|
|
|
|
db.records.filter(notDeleted).forEach(function (r) {
|
|
|
|
if (zone.name === r.zone) {
|
2018-02-01 02:19:11 +00:00
|
|
|
r.deletedAt = now;
|
2018-02-01 02:09:26 +00:00
|
|
|
records.push(r);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('[zone] [destroy] saving...');
|
|
|
|
db.save(function (err) {
|
|
|
|
zone.records = records;
|
|
|
|
cb(err, !err && zone || null);
|
|
|
|
});
|
|
|
|
}
|
2017-11-05 16:16:27 +00:00
|
|
|
};
|
2018-01-10 08:14:05 +00:00
|
|
|
engine.records = {
|
2018-01-10 08:25:56 +00:00
|
|
|
all: function (cb) {
|
2018-01-10 08:14:05 +00:00
|
|
|
process.nextTick(function () {
|
2018-01-29 21:54:03 +00:00
|
|
|
cb(null, db.records.slice(0).filter(notDeleted));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
, one: function (id, cb) {
|
|
|
|
var myRecord;
|
|
|
|
db.records.slice(0).some(function (r) {
|
|
|
|
if (id && id === r.id) {
|
|
|
|
if (notDeleted(r)) {
|
|
|
|
myRecord = r;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
process.nextTick(function () {
|
|
|
|
cb(null, myRecord);
|
2018-01-10 08:14:05 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
, get: function (query, cb) {
|
|
|
|
var myRecords = db.records.slice(0).filter(function (r) {
|
2017-11-05 16:16:27 +00:00
|
|
|
|
2018-01-10 08:14:05 +00:00
|
|
|
if ('string' !== typeof r.name) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-11-05 16:16:27 +00:00
|
|
|
|
2018-01-10 08:14:05 +00:00
|
|
|
// TODO use IN in masterquest (or implement OR)
|
|
|
|
// Only return single-level wildcard?
|
|
|
|
if (query.name === r.name || ('*.' + query.name.split('.').slice(1).join('.')) === r.name) {
|
2018-01-29 21:54:03 +00:00
|
|
|
if (notDeleted(r)) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-01-10 08:14:05 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
process.nextTick(function () {
|
|
|
|
cb(null, myRecords);
|
|
|
|
});
|
|
|
|
}
|
2018-01-23 22:21:20 +00:00
|
|
|
, save: function (record, cb) {
|
2018-01-26 08:42:24 +00:00
|
|
|
function touchZone(err, r) {
|
|
|
|
if (err) { cb(err); }
|
|
|
|
if (!r) { cb(null, null); }
|
|
|
|
engine.zones.touch({ name: r.zone }, cb);
|
|
|
|
}
|
|
|
|
|
2018-01-23 22:21:20 +00:00
|
|
|
if (record.id) {
|
|
|
|
console.log('update record!');
|
2018-01-26 08:42:24 +00:00
|
|
|
engine.records.update(record, touchZone);
|
2018-01-23 22:21:20 +00:00
|
|
|
} else {
|
2018-01-26 08:42:24 +00:00
|
|
|
engine.records.create(record, touchZone);
|
2018-01-23 22:21:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
, update: function (record, cb) {
|
|
|
|
var existing;
|
|
|
|
var dirty;
|
|
|
|
|
|
|
|
db.records.some(function (r) {
|
|
|
|
if (r.id === record.id) {
|
|
|
|
existing = r;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
console.log('no existing record');
|
|
|
|
cb(new Error("record for '" + record.id + "' does not exist"), null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('found existing record');
|
|
|
|
console.log(existing);
|
|
|
|
console.log(record);
|
|
|
|
Object.keys(record).forEach(function (key) {
|
2018-01-26 08:42:24 +00:00
|
|
|
var keys = [ 'name', 'id', 'zone', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ];
|
|
|
|
if (-1 !== keys.indexOf(key)) { return; }
|
2018-01-23 22:21:20 +00:00
|
|
|
if (existing[key] !== record[key]) {
|
|
|
|
dirty = true;
|
|
|
|
console.log(existing[key], record[key]);
|
|
|
|
existing[key] = record[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-01-26 08:42:24 +00:00
|
|
|
record.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000);
|
2018-01-23 22:21:20 +00:00
|
|
|
if (dirty) {
|
|
|
|
record.changedAt = record.updatedAt;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('saving...');
|
|
|
|
db.save(function (err) {
|
|
|
|
cb(err, !err && existing || null);
|
|
|
|
});
|
|
|
|
}
|
2018-01-26 08:42:24 +00:00
|
|
|
, create: function (record, cb) {
|
|
|
|
var obj = { id: crypto.randomBytes(16).toString('hex') };
|
|
|
|
console.log('found existing record');
|
|
|
|
console.log(record);
|
|
|
|
//var keys = [ 'name', 'id', 'zone', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ];
|
|
|
|
//var okeys = [ 'name', 'zone', 'admin', 'data', 'expiration', 'minimum', 'serial', 'retry', 'refresh', 'ttl', 'type' ]; // primary
|
|
|
|
var okeys = [ 'name', 'zone', 'type', 'data', 'class', 'ttl', 'address'
|
|
|
|
, 'exchange', 'priority', 'port', 'value', 'tag', 'flag', 'aname' ];
|
|
|
|
okeys.forEach(function (key) {
|
|
|
|
if ('undefined' !== typeof record[key]) {
|
|
|
|
obj[key] = record[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
record.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000);
|
|
|
|
//record.changedAt = record.updatedAt;
|
|
|
|
record.insertedAt = record.updatedAt;
|
|
|
|
record.createdAt = record.updatedAt;
|
|
|
|
|
|
|
|
console.log('saving new...');
|
|
|
|
db.records.push(record);
|
|
|
|
db.save(function (err) {
|
|
|
|
cb(err, record);
|
|
|
|
});
|
|
|
|
}
|
2018-01-29 21:54:03 +00:00
|
|
|
, destroy: function (id, cb) {
|
|
|
|
var record;
|
|
|
|
db.records.some(function (r/*, i*/) {
|
|
|
|
if (id === r.id) {
|
|
|
|
record = r;
|
|
|
|
r.deletedAt = Date.now();
|
|
|
|
//record = db.records.splice(i, 1);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
process.nextTick(function () {
|
|
|
|
db.save(function (err) {
|
|
|
|
cb(err, record);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2017-11-05 16:16:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return engine;
|
|
|
|
};
|