'use strict'; const electron = require('electron'); const NeDB = require('nedb'); const fsPath = require('path'); const {DB_PERSIST_INTERVAL} = require('../constants'); const {generateId} = require('../util'); const {isDevelopment} = require('../appInfo'); const stats = require('./stats'); const settings = require('./settings'); const workspace = require('./workspace'); const environment = require('./environment'); const cookieJar = require('./cookieJar'); const requestGroup = require('./requestGroup'); const request = require('./request'); const response = require('./response'); module.exports.CHANGE_INSERT = 'insert'; module.exports.CHANGE_UPDATE = 'update'; module.exports.CHANGE_REMOVE = 'remove'; const BASE_MODEL_DEFAULTS = () => ({ modified: Date.now(), created: Date.now(), parentId: null }); const MODEL_ID_PREFIXES = { [stats.type]: stats.prefix, [settings.type]: settings.prefix, [workspace.type]: workspace.prefix, [environment.type]: environment.prefix, [cookieJar.type]: cookieJar.prefix, [requestGroup.type]: requestGroup.prefix, [request.type]: request.prefix, [response.type]: response.prefix }; module.exports.MODEL_DEFAULTS = { [stats.type]: stats.init, [settings.type]: settings.init, [workspace.type]: workspace.init, [environment.type]: environment.init, [cookieJar.type]: cookieJar.init, [requestGroup.type]: requestGroup.init, [request.type]: request.init, [response.type]: response.init }; module.exports.ALL_TYPES = Object.keys(module.exports.MODEL_DEFAULTS); let db = null; function getDBFilePath (modelType) { // NOTE: Do not EVER change this. EVER! const basePath = electron.remote.app.getPath('userData'); return fsPath.join(basePath, `insomnia.${modelType}.db`); } /** * Initialize the database. This should be called once on app start. * @returns {Promise} */ let initialized = false; module.exports.initDB = (config = {}, force = false) => { // Only init once if (initialized && !force) { return Promise.resolve(); } console.log('CONFIG', config); return new Promise(resolve => { db = {}; if (isDevelopment()) { global.db = db; } // Fill in the defaults const modelTypes = Object.keys(module.exports.MODEL_DEFAULTS); modelTypes.map(t => { const filename = getDBFilePath(t); const autoload = true; const finalConfig = Object.assign({filename, autoload}, config); db[t] = new NeDB(finalConfig); db[t].persistence.setAutocompactionInterval(DB_PERSIST_INTERVAL) }); // Done initialized = true; console.log(`-- Initialize DB at ${getDBFilePath('t')} --`); resolve(); }); }; let changeListeners = {}; module.exports.onChange = (id, callback) => { console.log(`-- Added DB Listener ${id} -- `); changeListeners[id] = callback; }; module.exports.offChange = (id) => { console.log(`-- Removed DB Listener ${id} -- `); delete changeListeners[id]; }; function notifyOfChange (event, doc) { Object.keys(changeListeners).map(k => changeListeners[k](event, doc)); } module.exports.getMostRecentlyModified = (type, query = {}) => { return new Promise(resolve => { db[type].find(query).sort({modified: -1}).limit(1).exec((err, docs) => { resolve(docs.length ? docs[0] : null); }) }) }; module.exports.find = (type, query = {}) => { return new Promise((resolve, reject) => { db[type].find(query, (err, rawDocs) => { if (err) { return reject(err); } const modelDefaults = module.exports.MODEL_DEFAULTS[type](); const docs = rawDocs.map(rawDoc => { return Object.assign({}, modelDefaults, rawDoc); }); resolve(docs); }); }); }; module.exports.all = type => { return module.exports.find(type); }; module.exports.getWhere = (type, query) => { return new Promise((resolve, reject) => { db[type].find(query, (err, rawDocs) => { if (err) { return reject(err); } if (rawDocs.length === 0) { // Not found. Too bad! return resolve(null); } const modelDefaults = module.exports.MODEL_DEFAULTS[type](); resolve(Object.assign({}, modelDefaults, rawDocs[0])); }); }); }; module.exports.get = (type, id) => { return module.exports.getWhere(type, {_id: id}); }; module.exports.count = (type, query = {}) => { return new Promise((resolve, reject) => { db[type].count(query, (err, count) => { if (err) { return reject(err); } resolve(count); }); }); }; module.exports.insert = doc => { return new Promise((resolve, reject) => { db[doc.type].insert(doc, (err, newDoc) => { if (err) { return reject(err); } resolve(newDoc); notifyOfChange(module.exports.CHANGE_INSERT, doc); }); }); }; module.exports.update = doc => { return new Promise((resolve, reject) => { db[doc.type].update({_id: doc._id}, doc, err => { if (err) { return reject(err); } resolve(doc); notifyOfChange(module.exports.CHANGE_UPDATE, doc); }); }); }; module.exports.remove = doc => { return new Promise(resolve => { module.exports.withDescendants(doc).then(docs => { const promises = docs.map(d => ( db[d.type].remove({_id: d._id}, {multi: true}) )); Promise.all(promises).then(() => { docs.map(d => notifyOfChange(module.exports.CHANGE_REMOVE, d)); resolve() }); }); }); }; /** * Remove a lot of documents quickly and silently * * @param type * @param query * @returns {Promise.} */ module.exports.removeBulkSilently = (type, query) => { return new Promise(resolve => { db[type].remove(query, {multi: true}, err => resolve()); }); }; // ~~~~~~~~~~~~~~~~~~~ // // DEFAULT MODEL STUFF // // ~~~~~~~~~~~~~~~~~~~ // module.exports.docUpdate = (originalDoc, patch = {}) => { const doc = Object.assign( BASE_MODEL_DEFAULTS(), originalDoc, patch, {modified: Date.now()} ); return module.exports.update(doc); }; module.exports.docCreate = (type, patch = {}) => { const idPrefix = MODEL_ID_PREFIXES[type]; if (!idPrefix) { throw new Error(`No ID prefix for ${type}`) } const doc = Object.assign( BASE_MODEL_DEFAULTS(), {_id: generateId(idPrefix)}, module.exports.MODEL_DEFAULTS[type](), patch, // Fields that the user can't touch { type: type, modified: Date.now() } ); return module.exports.insert(doc); }; // ~~~~~~~ // // GENERAL // // ~~~~~~~ // module.exports.withDescendants = (doc = null) => { let docsToReturn = doc ? [doc] : []; const next = (docs) => { const promises = []; for (const doc of docs) { for (const type of module.exports.ALL_TYPES) { // If the doc is null, we want to search for parentId === null const parentId = doc ? doc._id : null; const promise = module.exports.find(type, {parentId}); promises.push(promise); } } return Promise.all(promises).then(results => { let newDocs = []; // Gather up the docs = require(each type for (const docs of results) { for (const doc of docs) { newDocs.push(doc); } } if (newDocs.length === 0) { // Didn't find anything. We're done return new Promise(resolve => resolve(docsToReturn)); } // Continue searching for children docsToReturn = [...docsToReturn, ...newDocs]; return next(newDocs); }); }; return next([doc]); }; module.exports.duplicate = (originalDoc, patch = {}) => { return new Promise((resolve, reject) => { // 1. Copy the doc const newDoc = Object.assign({}, originalDoc, patch); delete newDoc._id; delete newDoc.created; delete newDoc.modified; module.exports.docCreate(newDoc.type, newDoc).then(createdDoc => { // 2. Get all the children const promises = []; for (const type of module.exports.ALL_TYPES) { const parentId = originalDoc._id; const promise = module.exports.find(type, {parentId}); promises.push(promise); } Promise.all(promises).then(results => { let duplicatePromises = []; // Gather up the docs = require(each type for (const docs of results) { for (const doc of docs) { const promise = module.exports.duplicate( doc, {parentId: createdDoc._id} ); duplicatePromises.push(promise); } } // 3. Also duplicate all children, and recurse Promise.all(duplicatePromises).then(() => resolve(createdDoc), reject) }) }) }) }; // ~~~~~~ // // MODELS // // ~~~~~~ // module.exports.settings = settings; module.exports.stats = stats; module.exports.workspace = workspace; module.exports.cookieJar = cookieJar; module.exports.environment = environment; module.exports.request = request; module.exports.requestGroup = requestGroup; module.exports.response = response;