insomnia/app/backend/database/index.js
2016-09-20 17:03:26 -07:00

726 lines
17 KiB
JavaScript

'use strict';
const electron = require('electron');
const NeDB = require('nedb');
const fsPath = require('path');
const {
METHOD_GET,
DB_PERSIST_INTERVAL,
DEFAULT_SIDEBAR_WIDTH
} = require('../constants');
const {generateId} = require('../util');
const {PREVIEW_MODE_SOURCE} = require('../previewModes');
const {isDevelopment} = require('../appInfo');
module.exports.TYPE_STATS = 'Stats';
module.exports.TYPE_SETTINGS = 'Settings';
module.exports.TYPE_WORKSPACE = 'Workspace';
module.exports.TYPE_ENVIRONMENT = 'Environment';
module.exports.TYPE_COOKIE_JAR = 'CookieJar';
module.exports.TYPE_REQUEST_GROUP = 'RequestGroup';
module.exports.TYPE_REQUEST = 'Request';
module.exports.TYPE_RESPONSE = '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 = {
[module.exports.TYPE_STATS]: 'sta',
[module.exports.TYPE_SETTINGS]: 'set',
[module.exports.TYPE_WORKSPACE]: 'wrk',
[module.exports.TYPE_ENVIRONMENT]: 'env',
[module.exports.TYPE_COOKIE_JAR]: 'jar',
[module.exports.TYPE_REQUEST_GROUP]: 'fld',
[module.exports.TYPE_REQUEST]: 'req',
[module.exports.TYPE_RESPONSE]: 'res'
};
module.exports.MODEL_DEFAULTS = {
[module.exports.TYPE_STATS]: () => ({
lastLaunch: Date.now(),
lastVersion: null,
launches: 0
}),
[module.exports.TYPE_SETTINGS]: () => ({
showPasswords: true,
useBulkHeaderEditor: false,
followRedirects: false,
editorFontSize: 12,
editorLineWrapping: true,
httpProxy: '',
httpsProxy: '',
timeout: 0,
validateSSL: true
}),
[module.exports.TYPE_WORKSPACE]: () => ({
name: 'New Workspace',
metaSidebarWidth: DEFAULT_SIDEBAR_WIDTH,
metaActiveEnvironmentId: null,
metaActiveRequestId: null,
metaFilter: '',
metaSidebarHidden: false
}),
[module.exports.TYPE_ENVIRONMENT]: () => ({
name: 'New Environment',
data: {},
}),
[module.exports.TYPE_COOKIE_JAR]: () => ({
name: 'Default Jar',
cookies: []
}),
[module.exports.TYPE_REQUEST_GROUP]: () => ({
name: 'New Folder',
environment: {},
metaCollapsed: false,
metaSortKey: -1 * Date.now()
}),
[module.exports.TYPE_REQUEST]: () => ({
url: '',
name: 'New Request',
method: METHOD_GET,
body: '',
parameters: [],
headers: [],
authentication: {},
metaPreviewMode: PREVIEW_MODE_SOURCE,
metaResponseFilter: '',
metaSortKey: -1 * Date.now()
}),
[module.exports.TYPE_RESPONSE]: () => ({
statusCode: 0,
statusMessage: '',
contentType: 'text/plain',
url: '',
bytesRead: 0,
elapsedTime: 0,
headers: [],
cookies: [],
body: '',
error: ''
}),
};
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));
}
function 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);
})
})
}
function 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);
});
});
}
function all (type) {
return find(type);
}
function 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]));
});
});
}
function get (type, id) {
return getWhere(type, {_id: id});
}
function 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);
});
});
};
function 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);
});
});
}
function 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.<T>}
*/
function removeBulkSilently (type, query) {
return new Promise(resolve => {
db[type].remove(query, {multi: true}, err => resolve());
});
}
// ~~~~~~~~~~~~~~~~~~~ //
// DEFAULT MODEL STUFF //
// ~~~~~~~~~~~~~~~~~~~ //
function docUpdate (originalDoc, patch = {}) {
const doc = Object.assign(
BASE_MODEL_DEFAULTS(),
originalDoc,
patch,
{modified: Date.now()}
);
return update(doc);
}
function 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;
promises.push(find(type, {parentId}));
}
}
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;
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 = 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)
})
})
})
};
// ~~~~~~~ //
// REQUEST //
// ~~~~~~~ //
module.exports.requestCreateAndActivate = (workspace, patch = {}) => {
return module.exports.requestCreate(patch).then(r => {
module.exports.workspaceUpdate(workspace, {metaActiveRequestId: r._id});
})
};
module.exports.requestDuplicateAndActivate = (workspace, request) => {
return module.exports.requestDuplicate(request).then(r => {
module.exports.workspaceUpdate(workspace, {metaActiveRequestId: r._id});
})
};
module.exports.requestCreate = (patch = {}) => {
if (!patch.parentId) {
throw new Error('New Requests missing `parentId`', patch);
}
return docCreate(module.exports.TYPE_REQUEST, patch);
};
module.exports.requestGetById = id => {
return get(module.exports.TYPE_REQUEST, id);
};
module.exports.requestFindByParentId = parentId => {
return find(module.exports.TYPE_REQUEST, {parentId: parentId});
};
module.exports.requestUpdate = (request, patch) => {
return docUpdate(request, patch);
};
module.exports.requestUpdateContentType = (request, contentType) => {
let headers = [...request.headers];
const contentTypeHeader = headers.find(
h => h.name.toLowerCase() === 'content-type'
);
if (!contentType) {
// Remove the contentType header if we are unsetting it
headers = headers.filter(h => h !== contentTypeHeader);
} else if (contentTypeHeader) {
contentTypeHeader.value = contentType;
} else {
headers.push({name: 'Content-Type', value: contentType})
}
return docUpdate(request, {headers});
};
module.exports.requestDuplicate = request => {
const name = `${request.name} (Copy)`;
return module.exports.duplicate(request, {name});
};
module.exports.requestRemove = request => {
return remove(request);
};
module.exports.requestAll = () => {
return all(module.exports.TYPE_REQUEST);
};
module.exports.requestGetAncestors = request => {
return new Promise(resolve => {
let ancestors = [];
const next = (doc) => {
Promise.all([
module.exports.requestGroupGetById(doc.parentId),
module.exports.workspaceGetById(doc.parentId)
]).then(([requestGroup, workspace]) => {
if (requestGroup) {
ancestors = [requestGroup, ...ancestors];
next(requestGroup);
} else if (workspace) {
ancestors = [workspace, ...ancestors];
next(workspace);
// We could be done here, but let's have there only be one finish case
} else {
// We're finished
resolve(ancestors);
}
});
};
next(request);
});
};
// ~~~~~~~~~~~~~ //
// REQUEST GROUP //
// ~~~~~~~~~~~~~ //
module.exports.requestGroupCreate = (patch = {}) => {
if (!patch.parentId) {
throw new Error('New Requests missing `parentId`', patch);
}
return docCreate(module.exports.TYPE_REQUEST_GROUP, patch);
};
module.exports.requestGroupUpdate = (requestGroup, patch) => {
return docUpdate(requestGroup, patch);
};
module.exports.requestGroupGetById = id => {
return get(module.exports.TYPE_REQUEST_GROUP, id);
};
module.exports.requestGroupFindByParentId = parentId => {
return find(module.exports.TYPE_REQUEST_GROUP, {parentId});
};
module.exports.requestGroupRemove = requestGroup => {
return remove(requestGroup);
};
module.exports.requestGroupAll = () => {
return all(module.exports.TYPE_REQUEST_GROUP);
};
module.exports.requestGroupDuplicate = requestGroup => {
const name = `${requestGroup.name} (Copy)`;
return module.exports.duplicate(requestGroup, {name});
};
// ~~~~~~~~ //
// RESPONSE //
// ~~~~~~~~ //
module.exports.responseCreate = (patch = {}) => {
if (!patch.parentId) {
throw new Error('New Response missing `parentId`');
}
removeBulkSilently(module.exports.TYPE_RESPONSE, {parentId: patch.parentId});
return docCreate(module.exports.TYPE_RESPONSE, patch);
};
module.exports.responseGetLatestByParentId = parentId => {
return getMostRecentlyModified(module.exports.TYPE_RESPONSE, {parentId});
};
// ~~~~~~~ //
// COOKIES //
// ~~~~~~~ //
module.exports.cookieJarCreate = (patch = {}) => {
return docCreate(module.exports.TYPE_COOKIE_JAR, patch);
};
module.exports.cookieJarGetOrCreateForWorkspace = workspace => {
const parentId = workspace._id;
return find(module.exports.TYPE_COOKIE_JAR, {parentId}).then(cookieJars => {
if (cookieJars.length === 0) {
return module.exports.cookieJarCreate({parentId})
} else {
return new Promise(resolve => resolve(cookieJars[0]));
}
});
};
module.exports.cookieJarAll = () => {
return all(module.exports.TYPE_COOKIE_JAR);
};
module.exports.cookieJarGetById = id => {
return get(module.exports.TYPE_COOKIE_JAR, id);
};
module.exports.cookieJarUpdate = (cookieJar, patch) => {
return docUpdate(cookieJar, patch);
};
// ~~~~~~~~~ //
// WORKSPACE //
// ~~~~~~~~~ //
module.exports.workspaceGetById = id => {
return get(module.exports.TYPE_WORKSPACE, id);
};
module.exports.workspaceCreate = (patch = {}) => {
return docCreate(module.exports.TYPE_WORKSPACE, patch);
};
module.exports.workspaceAll = () => {
return all(module.exports.TYPE_WORKSPACE).then(workspaces => {
if (workspaces.length === 0) {
return module.exports.workspaceCreate({name: 'Insomnia'})
.then(module.exports.workspaceAll);
} else {
return new Promise(resolve => resolve(workspaces))
}
});
};
module.exports.workspaceCount = () => {
return count(module.exports.TYPE_WORKSPACE)
};
module.exports.workspaceUpdate = (workspace, patch) => {
return docUpdate(workspace, patch);
};
module.exports.workspaceRemove = workspace => {
return remove(workspace);
};
// ~~~~~~~~~~~ //
// ENVIRONMENT //
// ~~~~~~~~~~~ //
module.exports.environmentCreate = (patch = {}) => {
if (!patch.parentId) {
throw new Error('New Environment missing `parentId`', patch);
}
return docCreate(module.exports.TYPE_ENVIRONMENT, patch);
};
module.exports.environmentUpdate = (environment, patch) => {
return docUpdate(environment, patch);
};
module.exports.environmentFindByParentId = parentId => {
return find(module.exports.TYPE_ENVIRONMENT, {parentId});
};
module.exports.environmentGetOrCreateForWorkspace = workspace => {
const parentId = workspace._id;
return find(module.exports.TYPE_ENVIRONMENT, {parentId}).then(environments => {
if (environments.length === 0) {
return module.exports.environmentCreate({parentId, name: 'Base Environment'})
} else {
return new Promise(resolve => resolve(environments[0]));
}
});
};
module.exports.environmentGetById = id => {
return get(module.exports.TYPE_ENVIRONMENT, id);
};
module.exports.environmentRemove = environment => {
return remove(environment);
};
module.exports.environmentAll = () => {
return all(module.exports.TYPE_ENVIRONMENT);
};
// ~~~~~~~~ //
// SETTINGS //
// ~~~~~~~~ //
module.exports.settingsCreate = (patch = {}) => {
return docCreate(module.exports.TYPE_SETTINGS, patch);
};
module.exports.settingsUpdate = (settings, patch) => {
return docUpdate(settings, patch);
};
module.exports.settingsGetOrCreate = () => {
return all(module.exports.TYPE_SETTINGS).then(results => {
if (results.length === 0) {
return module.exports.settingsCreate()
.then(module.exports.settingsGetOrCreate);
} else {
return new Promise(resolve => resolve(results[0]));
}
});
};
// ~~~~~ //
// STATS //
// ~~~~~ //
module.exports.statsCreate = (patch = {}) => {
return docCreate(module.exports.TYPE_STATS, patch);
};
module.exports.statsUpdate = patch => {
return module.exports.statsGet().then(stats => {
return docUpdate(stats, patch);
});
};
module.exports.statsGet = () => {
return all(module.exports.TYPE_STATS).then(results => {
if (results.length === 0) {
return module.exports.statsCreate()
.then(module.exports.statsGet);
} else {
return new Promise(resolve => resolve(results[0]));
}
});
};