Switch to LokiJS (#12)

* LokiJS is working pretty well

* Tidy up
This commit is contained in:
Gregory Schier 2016-04-26 20:17:05 -07:00
parent aaacf89289
commit 9c11a7d61c
18 changed files with 215 additions and 150 deletions

2
.gitignore vendored
View File

@ -4,7 +4,7 @@ dist/*
build/* build/*
# PouchDB stuff # PouchDB stuff
*.db* *db.json
# Logs # Logs
logs logs

View File

@ -6,8 +6,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="./external/pouchdb.min.js"></script>
<script src="./external/pouchdb.find.min.js"></script>
<script> <script>
(function () { (function () {
const script = document.createElement('script'); const script = document.createElement('script');

View File

@ -60,7 +60,7 @@ class Sidebar extends Component {
<SidebarRequestGroupRow <SidebarRequestGroupRow
key={requestGroup._id} key={requestGroup._id}
isActive={isActive} isActive={isActive}
hideIfNoChildren={filter} hideIfNoChildren={!!filter}
toggleRequestGroup={this.props.toggleRequestGroup} toggleRequestGroup={this.props.toggleRequestGroup}
addRequestToRequestGroup={this.props.addRequestToRequestGroup} addRequestToRequestGroup={this.props.addRequestToRequestGroup}
numChildren={child.children.length} numChildren={child.children.length}

View File

@ -72,7 +72,7 @@ SidebarRequestGroupRow.propTypes = {
// Other // Other
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
hideIfNoChildren: PropTypes.number.isRequired, hideIfNoChildren: PropTypes.bool.isRequired,
requestGroup: PropTypes.object.isRequired requestGroup: PropTypes.object.isRequired
}; };

View File

@ -66,10 +66,10 @@ class App extends Component {
<div className="grid bg-super-dark tall"> <div className="grid bg-super-dark tall">
<Sidebar <Sidebar
workspaceId={workspace._id} workspaceId={workspace._id}
activateRequest={r => db.update(workspace, {activeRequestId: r._id})} activateRequest={r => db.workspaceUpdate(workspace, {activeRequestId: r._id})}
changeFilter={actions.requests.changeFilter} changeFilter={actions.requests.changeFilter}
addRequestToRequestGroup={requestGroup => db.requestCreate({parentId: requestGroup._id})} addRequestToRequestGroup={requestGroup => db.requestCreate({parentId: requestGroup._id})}
toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})} toggleRequestGroup={requestGroup => db.requestGroupUpdate(requestGroup, {collapsed: !requestGroup.collapsed})}
activeRequestId={activeRequest ? activeRequest._id : null} activeRequestId={activeRequest ? activeRequest._id : null}
filter={requests.filter} filter={requests.filter}
children={children} children={children}
@ -78,12 +78,12 @@ class App extends Component {
<RequestPane <RequestPane
request={activeRequest} request={activeRequest}
sendRequest={actions.requests.send} sendRequest={actions.requests.send}
updateRequestBody={body => db.update(activeRequest, {body})} updateRequestBody={body => db.requestUpdate(activeRequest, {body})}
updateRequestUrl={url => db.update(activeRequest, {url})} updateRequestUrl={url => db.requestUpdate(activeRequest, {url})}
updateRequestMethod={method => db.update(activeRequest, {method})} updateRequestMethod={method => db.requestUpdate(activeRequest, {method})}
updateRequestParams={params => db.update(activeRequest, {params})} updateRequestParams={params => db.requestUpdate(activeRequest, {params})}
updateRequestAuthentication={authentication => db.update(activeRequest, {authentication})} updateRequestAuthentication={authentication => db.requestUpdate(activeRequest, {authentication})}
updateRequestHeaders={headers => db.update(activeRequest, {headers})} updateRequestHeaders={headers => db.requestUpdate(activeRequest, {headers})}
/> />
<ResponsePane <ResponsePane
response={activeResponse} response={activeResponse}
@ -99,7 +99,7 @@ class App extends Component {
key={m.id} key={m.id}
requestGroup={m.data.requestGroup} requestGroup={m.data.requestGroup}
onClose={() => actions.modals.hide(m.id)} onClose={() => actions.modals.hide(m.id)}
onChange={rg => db.update(m.data.requestGroup, {environment: rg.environment})} onChange={rg => db.requestGroupUpdate(m.data.requestGroup, {environment: rg.environment})}
/> />
) )
} else { } else {

View File

@ -20,7 +20,7 @@ class Prompts extends Component {
header: 'Rename Request', header: 'Rename Request',
submit: 'Rename', submit: 'Rename',
onSubmit: (modal, name) => { onSubmit: (modal, name) => {
db.update(modal.data.request, {name}) db.requestUpdate(modal.data.request, {name})
} }
}; };
@ -28,7 +28,7 @@ class Prompts extends Component {
header: 'Rename Request Group', header: 'Rename Request Group',
submit: 'Rename', submit: 'Rename',
onSubmit: (modal, name) => { onSubmit: (modal, name) => {
db.update(modal.data.requestGroup, {name}) db.requestUpdate(modal.data.requestGroup, {name})
} }
}; };
@ -36,7 +36,7 @@ class Prompts extends Component {
header: 'Rename Workspace', header: 'Rename Workspace',
submit: 'Rename', submit: 'Rename',
onSubmit: (modal, name) => { onSubmit: (modal, name) => {
db.update(modal.data.workspace, {name}) db.requestUpdate(modal.data.workspace, {name})
} }
}; };
} }

View File

@ -31,7 +31,7 @@ class RequestActionsDropdown extends Component {
</button> </button>
</li> </li>
<li> <li>
<button onClick={e => db.remove(request)}> <button onClick={e => db.requestRemove(request)}>
<i className="fa fa-trash-o"></i> Delete <i className="fa fa-trash-o"></i> Delete
</button> </button>
</li> </li>

View File

@ -26,7 +26,7 @@ class RequestGroupActionsDropdown extends Component {
</button> </button>
</li> </li>
<li> <li>
<button onClick={e => db.remove(requestGroup)}> <button onClick={e => db.requestGroupRemove(requestGroup)}>
<i className="fa fa-trash-o"></i> Delete <i className="fa fa-trash-o"></i> Delete
</button> </button>
</li> </li>

View File

@ -97,7 +97,7 @@ class WorkspaceDropdown extends Component {
</button> </button>
</li> </li>
<li> <li>
<button onClick={e => db.remove(workspace)}> <button onClick={e => db.workspaceRemove(workspace)}>
<i className="fa fa-empty"></i> Delete <strong>{workspace.name}</strong> <i className="fa fa-empty"></i> Delete <strong>{workspace.name}</strong>
</button> </button>
</li> </li>

View File

@ -1,19 +1,73 @@
// import PouchDB from 'pouchdb'; // import PouchDB from 'pouchdb';
import * as methods from '../lib/constants'; import * as methods from '../lib/constants';
import {generateId} from './util' import {generateId} from './util'
import Loki from 'lokijs'
export const TYPE_WORKSPACE = 'Workspace'; export const TYPE_WORKSPACE = 'Workspace';
export const TYPE_REQUEST_GROUP = 'RequestGroup'; export const TYPE_REQUEST_GROUP = 'RequestGroup';
export const TYPE_REQUEST = 'Request'; export const TYPE_REQUEST = 'Request';
export const TYPE_RESPONSE = 'Response'; export const TYPE_RESPONSE = 'Response';
const TYPES = [
TYPE_WORKSPACE,
TYPE_REQUEST_GROUP,
TYPE_REQUEST,
TYPE_RESPONSE
];
// We have to include the web version of PouchDB in app.html because let db = null;
// the NodeJS version defaults to LevelDB which is hard (impossible?)
// to get working in Electron apps
let db = new PouchDB('insomnia.db', {adapter: 'websql'});
// For browser console debugging /**
global.db = db; * Initialize the database. This should be called once on app start.
* @returns {Promise}
*/
let initialized = false;
export function initDB () {
// Only init once
if (initialized) {
return new Promise(resolve => resolve());
}
return new Promise(resolve => {
db = new Loki('insomnia.db.json', {
autoload: true,
autosave: true,
autosaveInterval: 500, // TODO: Make this a bit smarter maybe
persistenceMethod: 'fs',
autoloadCallback () {
TYPES.map(type => {
let collection = db.getCollection(type);
if (!collection) {
collection = db.addCollection(type, {
asyncListeners: false,
clone: false,
disableChangesApi: false,
transactional: false
});
collection.ensureUniqueIndex('_id');
console.log(`-- Initialize DB Collection ${type} --`)
}
collection.on('update', doc => {
Object.keys(changeListeners).map(k => changeListeners[k]('update', doc));
});
collection.on('insert', doc => {
Object.keys(changeListeners).map(k => changeListeners[k]('insert', doc));
});
collection.on('delete', doc => {
Object.keys(changeListeners).map(k => changeListeners[k]('delete', doc));
});
});
resolve();
initialized = true;
}
});
})
}
let changeListeners = {}; let changeListeners = {};
@ -27,81 +81,65 @@ export function offChange (id) {
delete changeListeners[id]; delete changeListeners[id];
} }
export function allDocs () { export function get (type, id) {
return db.allDocs({include_docs: true}); const doc = db.getCollection(type).by('_id', id);
return new Promise(resolve => resolve(doc));
} }
db.changes({ function find (type, query) {
since: 'now', const docs = db.getCollection(type).find(query);
live: true, return new Promise(resolve => resolve(docs));
include_docs: true,
return_docs: false
}).on('change', function (res) {
Object.keys(changeListeners).map(id => changeListeners[id](res))
}).on('complete', function (info) {
console.log('complete', info);
}).on('error', function (err) {
console.log('error', err);
});
/**
* Initialize the database. This should be called once on app start.
* @returns {Promise}
*/
export function initDB () {
console.log('-- Initializing Database --');
return Promise.all([
db.createIndex({index: {fields: ['parentId']}}),
db.createIndex({index: {fields: ['type']}})
]).catch(err => {
console.error('Failed to PouchDB Indexes', err);
});
} }
export function get (id) { function insert (doc) {
return db.get(id); const newDoc = db.getCollection(doc.type).insert(doc);
return new Promise(resolve => resolve(newDoc));
} }
export function update (doc, patch = {}) { function update (doc) {
const updatedDoc = Object.assign( const newDoc = db.getCollection(doc.type).update(doc);
{}, return new Promise(resolve => resolve(newDoc));
doc,
patch,
{modified: Date.now()}
);
return db.put(updatedDoc).catch(e => {
if (e.status === 409) {
console.warn('Retrying document update for', updatedDoc);
get(doc._id).then(dbDoc => {
update(dbDoc, patch);
});
}
});
} }
function remove (doc) {
const newDoc = db.getCollection(doc.type).remove(doc);
return new Promise(resolve => resolve(newDoc));
}
// setInterval(() => {
// db.save();
// console.log('SAVED');
// }, 2000);
export function getChildren (doc) { export function getChildren (doc) {
const parentId = doc._id; return [];
return db.find({selector: {parentId}}); // const parentId = doc._id;
// return db.find({selector: {parentId}});
} }
export function removeChildren (doc) { export function removeChildren (doc) {
return getChildren(doc).then(res => res.docs.map(remove)); return [];
} // return getChildren(doc).then(res => res.docs.map(remove));
export function remove (doc) {
return Promise.all([
update(doc, {_deleted: true}),
removeChildren(doc)
]);
} }
// ~~~~~~~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~~~~~~~ //
// DEFAULT MODEL STUFF // // DEFAULT MODEL STUFF //
// ~~~~~~~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~~~~~~~ //
function modelCreate (type, idPrefix, defaults, patch = {}) { function docUpdate (originalDoc, patch = {}) {
const doc = Object.assign(
{},
originalDoc,
patch,
{modified: Date.now()}
);
// Fake a promise
const finalDoc = update(doc);
return new Promise(resolve => resolve(finalDoc));
}
function docCreate (type, idPrefix, defaults, patch = {}) {
const baseDefaults = { const baseDefaults = {
parentId: null parentId: null
}; };
@ -114,14 +152,16 @@ function modelCreate (type, idPrefix, defaults, patch = {}) {
// Required Generated Fields // Required Generated Fields
{ {
_id: generateId(idPrefix), _id: generateId(idPrefix),
_rev: undefined, $loki: undefined,
meta: undefined,
type: type, type: type,
created: Date.now(), created: Date.now(),
modified: Date.now() modified: Date.now()
} }
); );
return update(doc).then(() => doc); // Fake a promise
return insert(doc);
} }
// ~~~~~~~ // // ~~~~~~~ //
@ -129,7 +169,7 @@ function modelCreate (type, idPrefix, defaults, patch = {}) {
// ~~~~~~~ // // ~~~~~~~ //
export function requestCreate (patch = {}) { export function requestCreate (patch = {}) {
return modelCreate(TYPE_REQUEST, 'req', { return docCreate(TYPE_REQUEST, 'req', {
url: '', url: '',
name: 'New Request', name: 'New Request',
method: methods.METHOD_GET, method: methods.METHOD_GET,
@ -142,47 +182,79 @@ export function requestCreate (patch = {}) {
}, patch); }, patch);
} }
export function requestUpdate (request, patch) {
return docUpdate(request, patch);
}
export function requestCopy (request) { export function requestCopy (request) {
const name = `${request.name} (Copy)`; const name = `${request.name} (Copy)`;
return requestCreate(Object.assign({}, request, {name})); return requestCreate(Object.assign({}, request, {name}));
} }
export function requestRemove (request) {
return remove(request);
}
export function requestAll () {
return find(TYPE_REQUEST, {});
}
// ~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~ //
// REQUEST GROUP // // REQUEST GROUP //
// ~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~ //
export function requestGroupCreate (patch = {}) { export function requestGroupCreate (patch = {}) {
return modelCreate(TYPE_REQUEST_GROUP, 'grp', { return docCreate(TYPE_REQUEST_GROUP, 'grp', {
collapsed: false, collapsed: false,
name: 'New Request Group', name: 'New Request Group',
environment: {} environment: {}
}, patch); }, patch);
} }
export function requestGroupUpdate (requestGroup, patch) {
return docUpdate(requestGroup, patch);
}
export function requestGroupById (id) {
return get(TYPE_REQUEST_GROUP, id);
}
export function requestGroupRemove (requestGroup) {
return remove(requestGroup);
}
export function requestGroupAll () {
return find(TYPE_REQUEST_GROUP, {});
}
// ~~~~~~~~ // // ~~~~~~~~ //
// RESPONSE // // RESPONSE //
// ~~~~~~~~ // // ~~~~~~~~ //
export function responseCreate (patch = {}) { export function responseCreate (patch = {}) {
return modelCreate(TYPE_RESPONSE, 'res', { return docCreate(TYPE_RESPONSE, 'res', {
statusCode: 0, statusCode: 0,
statusMessage: '', statusMessage: '',
contentType: 'text/plain', contentType: 'text/plain',
bytes: 0, bytes: 0,
millis: 0, millis: 0,
headers: {}, headers: [],
body: '' body: ''
}, patch); }, patch);
} }
export function responseAll () {
return find(TYPE_RESPONSE, {});
}
// ~~~~~~~~~ // // ~~~~~~~~~ //
// WORKSPACE // // WORKSPACE //
// ~~~~~~~~~ // // ~~~~~~~~~ //
export function workspaceCreate (patch = {}) { export function workspaceCreate (patch = {}) {
return modelCreate(TYPE_WORKSPACE, 'wrk', { return docCreate(TYPE_WORKSPACE, 'wrk', {
name: 'New Workspace', name: 'New Workspace',
activeRequestId: null, activeRequestId: null,
environments: [] environments: []
@ -190,20 +262,22 @@ export function workspaceCreate (patch = {}) {
} }
export function workspaceAll () { export function workspaceAll () {
return db.find({ return find(TYPE_WORKSPACE, {}).then(workspaces => {
selector: {type: 'Workspace'} if (workspaces.length === 0) {
}).then(res => { workspaceCreate({name: 'Insomnia'});
if (res.docs.length) { return workspaceAll();
return res;
} else { } else {
// No workspaces? Create first one and try again return new Promise(resolve => resolve(workspaces))
// TODO: Replace this with UI flow maybe?
console.log('-- Creating First Workspace --');
return workspaceCreate({name: 'Insomnia'}).then(() => {
return workspaceAll();
})
} }
}) });
}
export function workspaceUpdate (workspace, patch) {
return docUpdate(workspace, patch);
}
export function workspaceRemove (workspace) {
return remove(workspace);
} }
// ~~~~~~~~ // // ~~~~~~~~ //
@ -211,9 +285,3 @@ export function workspaceAll () {
// ~~~~~~~~ // // ~~~~~~~~ //
// TODO: This // TODO: This
// export function settingsCreate (patch = {}) {
// return modelCreate('Settings', 'set', {
// editorLineWrapping: false,
// editorLineNumbers: true
// }, patch);
// }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -68,7 +68,7 @@ function actuallySend (unrenderedRequest, callback, context = {}) {
export function send (request, callback) { export function send (request, callback) {
if (request.parentId) { if (request.parentId) {
db.get(request.parentId).then( db.requestGroupById(request.parentId).then(
requestGroup => actuallySend(request, callback, requestGroup.environment) requestGroup => actuallySend(request, callback, requestGroup.environment)
); );
} else { } else {

View File

@ -7,6 +7,7 @@
"licence": "GPL-3.0", "licence": "GPL-3.0",
"main": "app.js", "main": "app.js",
"dependencies": { "dependencies": {
"lokijs": "^1.3.16",
"request": "^2.71.0" "request": "^2.71.0"
} }
} }

View File

@ -7,42 +7,41 @@ const CHANGE_ID = 'store.listener';
export function initStore (dispatch) { export function initStore (dispatch) {
db.offChange(CHANGE_ID); db.offChange(CHANGE_ID);
// New stuff...
const entities = bindActionCreators(entitiesActions, dispatch); const entities = bindActionCreators(entitiesActions, dispatch);
const docChanged = doc => { const docChanged = (event, doc) => {
if (!doc.hasOwnProperty('type')) { if (!doc.hasOwnProperty('type')) {
return; return;
} }
// New stuff... if (event === 'insert') {
entities[doc._deleted ? 'remove' : 'update'](doc); entities.insert(doc);
} else if (event === 'update') {
entities.update(doc);
} else if (event === 'delete') {
entities.remove(doc);
}
}; };
console.log('-- Restoring Store --'); console.log('-- Restoring Store --');
const start = Date.now(); const start = Date.now();
return db.workspaceAll().then(res => { // Restore docs in parent->child->grandchild order
const restoreChildren = (doc) => { return db.workspaceAll().then(docs => {
docChanged(doc); docs.map(doc => docChanged('update', doc));
return db.requestGroupAll();
return db.getChildren(doc).then(res => { }).then(docs => {
// Done condition docs.map(doc => docChanged('update', doc));
if (!res.docs.length) { return db.requestAll();
return; }).then(docs => {
} docs.map(doc => docChanged('update', doc));
return db.responseAll();
return Promise.all( }).then(docs => {
res.docs.map(doc => restoreChildren(doc)) docs.map(doc => docChanged('update', doc));
);
})
};
return res.docs.map(restoreChildren)
}).then(() => { }).then(() => {
console.log(`Restore took ${(Date.now() - start) / 1000} s`); console.log(`-- Restored DB in ${(Date.now() - start) / 1000} s --`);
}).then(() => { }).then(() => {
db.onChange(CHANGE_ID, res => docChanged(res.doc)); db.onChange(CHANGE_ID, docChanged);
}); });
} }

View File

@ -3,6 +3,7 @@ import {combineReducers} from 'redux'
import {TYPE_WORKSPACE, TYPE_REQUEST_GROUP, TYPE_REQUEST, TYPE_RESPONSE} from '../../database/index' import {TYPE_WORKSPACE, TYPE_REQUEST_GROUP, TYPE_REQUEST, TYPE_RESPONSE} from '../../database/index'
import * as workspaceFns from './workspaces' import * as workspaceFns from './workspaces'
const ENTITY_INSERT = 'entities/insert';
const ENTITY_UPDATE = 'entities/update'; const ENTITY_UPDATE = 'entities/update';
const ENTITY_REMOVE = 'entities/remove'; const ENTITY_REMOVE = 'entities/remove';
@ -21,6 +22,7 @@ function genericEntityReducer (referenceName) {
switch (action.type) { switch (action.type) {
case ENTITY_UPDATE: case ENTITY_UPDATE:
case ENTITY_INSERT:
return {...state, [doc._id]: doc}; return {...state, [doc._id]: doc};
case ENTITY_REMOVE: case ENTITY_REMOVE:
@ -46,6 +48,13 @@ export default combineReducers({
// ACTIONS // // ACTIONS //
// ~~~~~~~ // // ~~~~~~~ //
const insertFns = {
[TYPE_WORKSPACE]: workspace => ({type: ENTITY_INSERT, workspace}),
[TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_INSERT, requestGroup}),
[TYPE_RESPONSE]: response => ({type: ENTITY_INSERT, response}),
[TYPE_REQUEST]: request => ({type: ENTITY_INSERT, request})
};
const updateFns = { const updateFns = {
[TYPE_WORKSPACE]: workspace => ({type: ENTITY_UPDATE, workspace}), [TYPE_WORKSPACE]: workspace => ({type: ENTITY_UPDATE, workspace}),
[TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_UPDATE, requestGroup}), [TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_UPDATE, requestGroup}),
@ -60,6 +69,10 @@ const removeFns = {
[TYPE_REQUEST]: request => ({type: ENTITY_REMOVE, request}) [TYPE_REQUEST]: request => ({type: ENTITY_REMOVE, request})
}; };
export function insert (doc) {
return insertFns[doc.type](doc);
}
export function update (doc) { export function update (doc) {
return updateFns[doc.type](doc); return updateFns[doc.type](doc);
} }

View File

@ -9,7 +9,7 @@ NODE_ENV=production node -r babel-register ./node_modules/.bin/webpack --config
echo "-- COPYING REMAINING FILES --" echo "-- COPYING REMAINING FILES --"
cp -r app/package.json app/app.js app/external dist/ cp -r app/package.json app/app.js dist/
echo "-- INSTALLING PACKAGES --" echo "-- INSTALLING PACKAGES --"

View File

@ -60,10 +60,9 @@ export default {
packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'] packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main']
}, },
externals: [ externals: [
...Object.keys(pkg.dependencies),
{
} // Omit all the app package dependencies (we want them loaded at runtime via NodeJS)
...Object.keys(pkg.dependencies)
], ],
plugins: [], plugins: [],
target: 'electron-renderer' target: 'electron-renderer'