diff --git a/app/models/index.js b/app/models/index.js
index 5d0f83d46a..4b4e6aa4d8 100644
--- a/app/models/index.js
+++ b/app/models/index.js
@@ -7,6 +7,7 @@ import * as _cookieJar from './cookie-jar';
import * as _requestGroup from './request-group';
import * as _requestGroupMeta from './request-group-meta';
import * as _request from './request';
+import * as _requestVersion from './request-version';
import * as _requestMeta from './request-meta';
import * as _response from './response';
import * as _oAuth2Token from './o-auth-2-token';
@@ -21,6 +22,7 @@ export const cookieJar = _cookieJar;
export const requestGroup = _requestGroup;
export const requestGroupMeta = _requestGroupMeta;
export const request = _request;
+export const requestVersion = _requestVersion;
export const requestMeta = _requestMeta;
export const response = _response;
export const oAuth2Token = _oAuth2Token;
@@ -35,6 +37,7 @@ const _models = {
[requestGroup.type]: requestGroup,
[requestGroupMeta.type]: requestGroupMeta,
[request.type]: request,
+ [requestVersion.type]: requestVersion,
[requestMeta.type]: requestMeta,
[response.type]: response,
[oAuth2Token.type]: oAuth2Token
diff --git a/app/models/request-version.js b/app/models/request-version.js
new file mode 100644
index 0000000000..c1d5998479
--- /dev/null
+++ b/app/models/request-version.js
@@ -0,0 +1,117 @@
+import zlib from 'zlib';
+import deepEqual from 'deep-equal';
+import * as models from './index';
+import * as db from '../common/database';
+export const name = 'Request Version';
+export const type = 'RequestVersion';
+export const prefix = 'rvr';
+export const canDuplicate = false;
+
+const FIELDS_TO_IGNORE_IN_REQUEST_DIFF = [
+ '_id',
+ 'type',
+ 'created',
+ 'modified',
+ 'metaSortKey',
+ 'description',
+ 'name'
+];
+
+export function init () {
+ return {
+ compressedRequest: null
+ };
+}
+
+export function migrate (doc) {
+ return doc;
+}
+
+export function getById (id) {
+ return db.get(type, id);
+}
+
+export async function create (request) {
+ if (!request.type === models.request.type) {
+ throw new Error(`New ${type} was not given a valid ${models.request.type} instance`);
+ }
+
+ const parentId = request._id;
+ const latestRequestVersion = await getLatestByParentId(parentId);
+ const latestRequest = latestRequestVersion
+ ? await _decompressRequest(latestRequestVersion.compressedRequest)
+ : null;
+
+ const hasChanged = _diffRequests(latestRequest, request);
+ if (hasChanged) {
+ // Create a new version if the request has been modified
+ const compressedRequest = await _compressRequest(request);
+ return db.docCreate(type, {parentId, compressedRequest});
+ } else {
+ // Re-use the latest version if not modified since
+ return latestRequestVersion;
+ }
+}
+
+export function getLatestByParentId (parentId) {
+ return db.getMostRecentlyModified(type, {parentId});
+}
+
+export async function restore (requestVersionId) {
+ const requestVersion = await getById(requestVersionId);
+
+ // Older responses won't have versions saved with them
+ if (!requestVersion) {
+ return null;
+ }
+
+ const request = await _decompressRequest(requestVersion.compressedRequest);
+ return models.request.update(request);
+}
+
+function _diffRequests (rOld, rNew) {
+ if (!rOld) {
+ return true;
+ }
+
+ for (const key of Object.keys(rOld)) {
+ // Skip fields that aren't useful
+ if (FIELDS_TO_IGNORE_IN_REQUEST_DIFF.includes(key)) {
+ continue;
+ }
+
+ if (!deepEqual(rOld[key], rNew[key])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function _compressRequest (request) {
+ return new Promise((resolve, reject) => {
+ const str = JSON.stringify(request);
+ zlib.gzip(str, {}, (err, buffer) => {
+ if (err) {
+ reject(err);
+ } else {
+ const encoded = buffer.toString('base64');
+ resolve(encoded);
+ }
+ });
+ });
+}
+
+function _decompressRequest (requestJson) {
+ return new Promise((resolve, reject) => {
+ const buffer = Buffer.from(requestJson, 'base64');
+ zlib.gunzip(buffer, {}, (err, jsonStr) => {
+ if (err) {
+ reject(err);
+ } else {
+ const obj = JSON.parse(jsonStr);
+ resolve(obj);
+ }
+ });
+ });
+}
diff --git a/app/models/response.js b/app/models/response.js
index f1cdf7e4c8..743545ab60 100644
--- a/app/models/response.js
+++ b/app/models/response.js
@@ -1,5 +1,6 @@
import * as db from '../common/database';
import {MAX_RESPONSES} from '../common/constants';
+import * as models from './index';
export const name = 'Response';
export const type = 'Response';
@@ -20,6 +21,7 @@ export function init () {
body: '',
encoding: 'utf8', // Legacy format
error: '',
+ requestVersionId: null,
// Things from the request
settingStoreCookies: null,
@@ -63,6 +65,11 @@ export async function create (patch = {}) {
const {parentId} = patch;
+ // Create request version snapshot
+ const request = await models.request.getById(parentId);
+ const requestVersion = request ? await models.requestVersion.create(request) : null;
+ patch.requestVersionId = requestVersion ? requestVersion._id : null;
+
// Delete all other responses before creating the new one
const allResponses = await db.findMostRecentlyModified(type, {parentId}, MAX_RESPONSES);
const recentIds = allResponses.map(r => r._id);
diff --git a/app/package.json b/app/package.json
index 41ba1731a2..7a5f3ea09a 100644
--- a/app/package.json
+++ b/app/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"electron-context-menu": "0.9.0",
"electron-squirrel-startup": "1.0.0",
+ "deep-equal": "1.0.1",
"hkdf": "0.0.2",
"httpsnippet": "1.16.5",
"insomnia-importers": "1.3.8",
diff --git a/app/ui/components/dropdowns/response-history-dropdown.js b/app/ui/components/dropdowns/response-history-dropdown.js
index b0346ceb46..f38a434ffc 100644
--- a/app/ui/components/dropdowns/response-history-dropdown.js
+++ b/app/ui/components/dropdowns/response-history-dropdown.js
@@ -64,6 +64,8 @@ class ResponseHistoryDropdown extends PureComponent {
renderDropdownItem (response, i) {
const {activeResponseId} = this.props;
const active = response._id === activeResponseId;
+ const message = 'Request will not be restored with this response because ' +
+ 'it was created before this ability was added';
return (
+ {!response.requestVersionId && }
);
}
diff --git a/app/ui/containers/app.js b/app/ui/containers/app.js
index 5a37b7ef0e..6e84535edc 100644
--- a/app/ui/containers/app.js
+++ b/app/ui/containers/app.js
@@ -472,8 +472,26 @@ class App extends PureComponent {
this.props.handleStopLoading(requestId);
}
- _handleSetActiveResponse (requestId, activeResponseId) {
- this._updateRequestMetaByParentId(requestId, {activeResponseId});
+ async _handleSetActiveResponse (requestId, activeResponseId = null) {
+ await this._updateRequestMetaByParentId(requestId, {activeResponseId});
+
+ let response;
+ if (activeResponseId) {
+ response = await models.response.getById(activeResponseId);
+ } else {
+ response = await models.response.getLatestForRequest(requestId);
+ }
+
+ const requestVersionId = response ? response.requestVersionId : 'n/a';
+ const request = await models.requestVersion.restore(requestVersionId);
+
+ if (request) {
+ // Refresh app to reflect changes. Using timeout because we need to
+ // wait for the request update to propagate.
+ setTimeout(() => this._wrapper._forceRequestPaneRefresh(), 500);
+ } else {
+ // Couldn't restore request. That's okay
+ }
}
_requestCreateForWorkspace () {
diff --git a/package-lock.json b/package-lock.json
index f5f215966c..5bb75b3500 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1755,6 +1755,11 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
+ "deep-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+ "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
+ },
"deep-extend": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz",
diff --git a/package.json b/package.json
index 4d081c37e8..75110a7a7e 100644
--- a/package.json
+++ b/package.json
@@ -112,6 +112,7 @@
"classnames": "2.2.5",
"clone": "2.1.0",
"codemirror": "5.24.2",
+ "deep-equal": "1.0.1",
"electron-context-menu": "0.9.0",
"electron-squirrel-startup": "1.0.0",
"highlight.js": "9.12.0",