Scope responses to active environment (#1905)

* Scope responses to active environment

* Update response history to support scoped envs
This commit is contained in:
Gregory Schier authored and GitHub committed 2020-01-22 14:23:19 -05:00
1 parent 6200c9e8c7
commit de2fc0ba42
12 files changed
+230 -94

No files matched your search

@@ -137,6 +137,30 @@ describe('exportHar()', () => {
req3.headers.push({ name: 'X-Request', value: '3' });
await models.request.create(req3);
const envBase = await models.environment.getOrCreateForWorkspace(workspace);
await models.environment.update(envBase, {
data: {
envvalue: '',
},
});
const envPublic = await models.environment.create({
_id: 'env_1',
name: 'Public',
parentId: envBase._id,
data: {
envvalue: 'public',
},
});
const envPrivate = await models.environment.create({
_id: 'env_2',
name: 'Private',
isPrivate: true,
parentId: envBase._id,
data: {
envvalue: 'private',
},
});
await models.response.create({
_id: 'res_1',
parentId: req1._id,
@@ -153,36 +177,8 @@ describe('exportHar()', () => {
statusCode: 500,
});
const envBase = await models.environment.getOrCreateForWorkspace(workspace);
await models.environment.update(envBase, {
data: {
envvalue: '',
},
});
const envPublic = await models.environment.create({
_id: 'env_1',
name: 'Public',
parentId: envBase._id,
});
await models.environment.update(envPublic, {
data: {
envvalue: 'public',
},
});
const envPrivate = await models.environment.create({
_id: 'env_2',
name: 'Private',
isPrivate: true,
parentId: envBase._id,
});
await models.environment.update(envPrivate, {
data: {
envvalue: 'private',
},
});
const exportRequests = [
{ requestId: req1._id, environmentId: 'n/a' },
{ requestId: req1._id, environmentId: null },
{ requestId: req2._id, environmentId: envPublic._id },
{ requestId: req3._id, environmentId: envPrivate._id },
];
+3 -2
View File
@@ -171,7 +171,7 @@ export type Har = {
export type ExportRequest = {
requestId: string,
environmentId: string,
environmentId: string | null,
};
export async function exportHar(exportRequests: Array<ExportRequest>): Promise<Har> {
@@ -191,6 +191,7 @@ export async function exportHar(exportRequests: Array<ExportRequest>): Promise<H
const response: ResponseModel | null = await models.response.getLatestForRequest(
exportRequest.requestId,
exportRequest.environmentId || null,
);
const harResponse = await exportHarResponse(response);
if (!harResponse) {
@@ -276,7 +277,7 @@ export async function exportHarRequest(
export async function exportHarWithRequest(
request: Request,
environmentId: string,
environmentId: string | null,
addContentLength: boolean = false,
): Promise<HarRequest | null> {
try {
+81 -11
View File
@@ -28,6 +28,7 @@ export type ResponseTimelineEntry = {
};
type BaseResponse = {
environmentId: string | null,
statusCode: number,
statusMessage: string,
httpVersion: string,
@@ -52,6 +53,7 @@ export type Response = BaseModel & BaseResponse;
export function init(): BaseResponse {
return {
environmentId: '__LEGACY__',
statusCode: 0,
statusMessage: '',
httpVersion: '',
@@ -80,10 +82,14 @@ export async function migrate(doc: Object) {
return doc;
}
export async function hookDatabaseInit() {
await models.response.cleanDeletedResponses();
export function hookDatabaseInit() {
console.log('Init responses DB');
process.nextTick(async () => {
await models.response.cleanDeletedResponses();
// Can remove this after March 2020
await addLegacyEnvironmentId();
});
}
export function hookRemove(doc: Response) {
@@ -103,24 +109,57 @@ export async function all(): Promise<Array<Response>> {
return db.all(type);
}
export async function removeForRequest(parentId: string) {
await db.removeWhere(type, { parentId });
export async function removeForRequest(parentId: string, environmentId?: string | null) {
const q: Object = {
parentId,
};
// Only add if not undefined. null is not the same as undefined
// null: find responses sent from base environment
// undefined: find all responses
if (environmentId !== undefined) {
q.environmentId = environmentId;
}
// Also delete legacy responses here or else the user will be confused as to
// why some responses are still showing in the UI.
await db.removeWhere(type, { $or: [q, { parentId, environmentId: '__LEGACY__' }] });
}
export function remove(response: Response) {
return db.remove(response);
}
export async function findRecentForRequest(
async function _findRecentForRequest(
requestId: string,
environmentId: string | null,
limit: number,
): Promise<Array<Response>> {
const responses = await db.findMostRecentlyModified(type, { parentId: requestId }, limit);
return responses;
const q = {
parentId: requestId,
environmentId: environmentId,
};
return db.findMostRecentlyModified(
type,
{
$or: [
q,
{
parentId: requestId,
environmentId: '__LEGACY__',
},
],
},
limit,
);
}
export async function getLatestForRequest(requestId: string): Promise<Response | null> {
const responses = await findRecentForRequest(requestId, 1);
export async function getLatestForRequest(
requestId: string,
environmentId: string | null,
): Promise<Response | null> {
const responses = await _findRecentForRequest(requestId, environmentId, 1);
const response = (responses[0]: ?Response);
return response || null;
}
@@ -283,6 +322,33 @@ async function migrateTimelineToFileSystem(doc: Object) {
}
}
async function addLegacyEnvironmentId() {
const all = await db.all(type);
// Bail early if none yet. This is mostly here so tests don't
// get impacted
if (all.length === 0) {
return;
}
// We need to query on the __LEGACY__ value so we need
// to migrate it into the DB to make that possible. If we
// don't do that, it will exist on queried document by default
// but won't actually be in the DB.
const promises = [];
const flushId = await db.bufferChanges();
for (const doc of all) {
if (doc.environmentId !== '__LEGACY__') {
continue;
}
promises.push(db.docUpdate(doc));
}
await Promise.all(promises);
await db.flushChanges(flushId);
}
export async function cleanDeletedResponses() {
const responsesDir = path.join(getDataDirectory(), 'responses');
mkdirp.sync(responsesDir);
@@ -303,6 +369,10 @@ export async function cleanDeletedResponses() {
continue;
}
fs.unlinkSync(path.join(responsesDir, filePath));
try {
fs.unlinkSync(path.join(responsesDir, filePath));
} catch (err) {
// Just keep going, doesn't matter
}
}
}
+48 -24
View File
@@ -60,25 +60,27 @@ import { cookiesFromJar, jarFromCookies } from 'insomnia-cookies';
import { urlMatchesCertHost } from './url-matches-cert-host';
import aws4 from 'aws4';
import { buildMultipart } from './multipart';
import type { Environment } from '../models/environment';
export type ResponsePatch = {|
statusMessage?: string,
error?: string,
url?: string,
statusCode?: number,
bytesContent?: number,
bodyPath?: string,
bodyCompression?: 'zip' | null,
message?: string,
httpVersion?: string,
headers?: Array<ResponseHeader>,
elapsedTime?: number,
contentType?: string,
bodyPath?: string,
bytesContent?: number,
bytesRead?: number,
contentType?: string,
elapsedTime?: number,
environmentId?: string | null,
error?: string,
headers?: Array<ResponseHeader>,
httpVersion?: string,
message?: string,
parentId?: string,
settingStoreCookies?: boolean,
settingSendCookies?: boolean,
settingStoreCookies?: boolean,
statusCode?: number,
statusMessage?: string,
timelinePath?: string,
url?: string,
|};
// Time since user's last keypress to wait before making the request
@@ -101,6 +103,7 @@ export async function _actuallySend(
renderContext: Object,
workspace: Workspace,
settings: Settings,
environment: Environment | null,
): Promise<ResponsePatch> {
return new Promise(async resolve => {
let timeline: Array<ResponseTimelineEntry> = [];
@@ -124,9 +127,11 @@ export async function _actuallySend(
): Promise<void> {
const timelinePath = await storeTimeline(timeline);
const environmentId = environment ? environment._id : null;
const responsePatchBeforeHooks = Object.assign(
({
timelinePath,
environmentId,
parentId: renderedRequest._id,
bodyCompression: null, // Will default to .zip otherwise
bodyPath: bodyPath || '',
@@ -719,14 +724,14 @@ export async function _actuallySend(
// Return the response data
const responsePatch = {
headers,
contentType,
statusCode,
headers,
httpVersion,
statusCode,
statusMessage,
elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) * 1000,
bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD),
bytesContent: responseBodyBytes,
bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD),
elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) * 1000,
url: curl.getInfo(Curl.info.EFFECTIVE_URL),
};
@@ -749,7 +754,14 @@ export async function _actuallySend(
statusMessage = 'Abort';
}
respond({ statusMessage, error }, null, true);
respond(
{
statusMessage,
error,
},
null,
true,
);
});
curl.perform();
@@ -790,6 +802,8 @@ export async function sendWithSettings(
parentId: request._id,
});
const environment: Environment | null = await models.environment.getById(environmentId || 'n/a');
let renderResult: { request: RenderedRequest, context: Object };
try {
renderResult = await getRenderedRequestAndContext(newRequest, environmentId);
@@ -797,12 +811,18 @@ export async function sendWithSettings(
throw new Error(`Failed to render request: ${requestId}`);
}
return _actuallySend(renderResult.request, renderResult.context, workspace, settings);
return _actuallySend(
renderResult.request,
renderResult.context,
workspace,
settings,
environment,
);
}
export async function send(
requestId: string,
environmentId: string | null,
environmentId?: string,
extraInfo?: ExtraRenderInfo,
): Promise<ResponsePatch> {
console.log(`[network] Sending req=${requestId}`);
@@ -834,9 +854,11 @@ export async function send(
throw new Error(`Failed to find request to send for ${requestId}`);
}
const environment: Environment | null = await models.environment.getById(environmentId || 'n/a');
const renderResult = await getRenderedRequestAndContext(
request,
environmentId,
environmentId || null,
RENDER_PURPOSE_SEND,
extraInfo,
);
@@ -858,13 +880,14 @@ export async function send(
);
} catch (err) {
return {
url: renderedRequestBeforePlugins.url,
parentId: renderedRequestBeforePlugins._id,
environmentId: environmentId,
error: err.message,
statusCode: STATUS_CODE_PLUGIN_ERROR,
statusMessage: err.plugin ? `Plugin ${err.plugin.name}` : 'Plugin',
parentId: renderedRequestBeforePlugins._id,
settingSendCookies: renderedRequestBeforePlugins.settingSendCookies,
settingStoreCookies: renderedRequestBeforePlugins.settingStoreCookies,
statusCode: STATUS_CODE_PLUGIN_ERROR,
statusMessage: err.plugin ? `Plugin ${err.plugin.name}` : 'Plugin',
url: renderedRequestBeforePlugins.url,
};
}
@@ -873,6 +896,7 @@ export async function send(
renderedContextBeforePlugins,
workspace,
settings,
environment,
);
console.log(
@@ -8,7 +8,7 @@ import type { ExtraRenderInfo } from '../../common/render';
export function init(activeEnvironmentId: string | null): { network: Object } {
const network = {
async sendRequest(request: Request, extraInfo?: ExtraRenderInfo): Promise<Response> {
const responsePatch = await send(request._id, activeEnvironmentId, extraInfo);
const responsePatch = await send(request._id, activeEnvironmentId || undefined, extraInfo);
const settings = await models.settings.getOrCreate();
return models.response.create(responsePatch, settings.maxHistoryResponses);
},
@@ -15,15 +15,17 @@ import TimeFromNow from '../time-from-now';
import type { Response } from '../../../models/response';
import type { RequestVersion } from '../../../models/request-version';
import { decompressObject } from '../../../common/misc';
import type { Environment } from '../../../models/environment';
type Props = {
handleSetActiveResponse: Response => Promise<void>,
handleDeleteResponses: (requestId: string) => Promise<void>,
handleDeleteResponses: (requestId: string, environmentId: string | null) => Promise<void>,
handleDeleteResponse: Response => Promise<void>,
requestId: string,
responses: Array<Response>,
requestVersions: Array<RequestVersion>,
activeResponse: Response,
activeEnvironment: ?Environment,
};
@autobind
@@ -35,7 +37,9 @@ class ResponseHistoryDropdown extends React.PureComponent<Props> {
}
_handleDeleteResponses() {
this.props.handleDeleteResponses(this.props.requestId);
const { requestId, activeEnvironment } = this.props;
const environmentId = activeEnvironment ? activeEnvironment._id : null;
this.props.handleDeleteResponses(requestId, environmentId);
}
_handleDeleteResponse() {
@@ -97,34 +101,39 @@ class ResponseHistoryDropdown extends React.PureComponent<Props> {
renderPastResponses(responses: Array<Response>) {
const now = moment();
// Four arrays for four time groups
const categories = [[], [], [], []];
const categories = { minutes: [], hours: [], today: [], week: [], other: [] };
responses.forEach(r => {
const resTime = moment(r.modified);
const resTime = moment(r.created);
if (now.diff(resTime, 'minutes') < 5) {
// Five minutes ago
categories[0].push(r);
categories.minutes.push(r);
} else if (now.diff(resTime, 'hours') < 2) {
// Two hours ago
categories[1].push(r);
categories.hours.push(r);
} else if (now.isSame(resTime, 'day')) {
// Today
categories[2].push(r);
categories.today.push(r);
} else if (now.isSame(resTime, 'week')) {
// This week
categories[3].push(r);
categories.week.push(r);
} else {
// Older
categories.other.push(r);
}
});
return (
<React.Fragment>
<DropdownDivider>5 Minutes Ago</DropdownDivider>
{categories[0].map(this.renderDropdownItem)}
{categories.minutes.map(this.renderDropdownItem)}
<DropdownDivider>2 Hours Ago</DropdownDivider>
{categories[1].map(this.renderDropdownItem)}
{categories.hours.map(this.renderDropdownItem)}
<DropdownDivider>Today</DropdownDivider>
{categories[2].map(this.renderDropdownItem)}
{categories.today.map(this.renderDropdownItem)}
<DropdownDivider>This Week</DropdownDivider>
{categories[3].map(this.renderDropdownItem)}
{categories.week.map(this.renderDropdownItem)}
<DropdownDivider>Older Than This Week</DropdownDivider>
{categories.other.map(this.renderDropdownItem)}
</React.Fragment>
);
}
@@ -136,9 +145,12 @@ class ResponseHistoryDropdown extends React.PureComponent<Props> {
handleDeleteResponses, // eslint-disable-line no-unused-vars
handleDeleteResponse, // eslint-disable-line no-unused-vars
responses,
activeEnvironment,
...extraProps
} = this.props;
const environmentName = activeEnvironment ? activeEnvironment.name : 'Base';
const isLatestResponseActive = !responses.length || activeResponse._id === responses[0]._id;
return (
@@ -155,7 +167,9 @@ class ResponseHistoryDropdown extends React.PureComponent<Props> {
<i className="fa fa-caret-down space-left" />
)}
</DropdownButton>
<DropdownDivider>Response History</DropdownDivider>
<DropdownDivider>
<strong>{environmentName}</strong> Responses
</DropdownDivider>
<DropdownItem buttonClass={PromptButton} addIcon onClick={this._handleDeleteResponse}>
<i className="fa fa-trash-o" />
Delete Current Response
@@ -30,6 +30,7 @@ import { hotKeyRefs } from '../../common/hotkeys';
import type { RequestVersion } from '../../models/request-version';
import { showError } from '../components/modals/index';
import { json as jsonPrettify } from 'insomnia-prettify';
import type { Environment } from '../../models/environment';
type Props = {
// Functions
@@ -59,6 +60,7 @@ type Props = {
requestVersions: Array<RequestVersion>,
request: ?Request,
response: ?Response,
environment: ?Environment,
};
@autobind
@@ -183,6 +185,7 @@ class ResponsePane extends React.PureComponent<Props> {
editorIndentSize,
editorKeyMap,
editorLineWrapping,
environment,
filter,
disableResponsePreviewLinks,
filterHistory,
@@ -289,6 +292,7 @@ class ResponsePane extends React.PureComponent<Props> {
</div>
<ResponseHistoryDropdown
activeResponse={response}
activeEnvironment={environment}
responses={responses}
requestVersions={requestVersions}
requestId={request._id}
@@ -308,14 +308,14 @@ class Wrapper extends React.PureComponent<Props, State> {
showModal(RequestSettingsModal, { request: this.props.activeRequest });
}
async _handleDeleteResponses(): Promise<void> {
if (!this.props.activeRequest) {
console.warn('Tried to delete responses when request not active');
return;
}
async _handleDeleteResponses(requestId: string, environmentId: string | null): Promise<void> {
const { handleSetActiveResponse, activeRequest } = this.props;
await models.response.removeForRequest(this.props.activeRequest._id);
this._handleSetActiveResponse(null);
await models.response.removeForRequest(requestId, environmentId);
if (activeRequest && activeRequest._id === requestId) {
await handleSetActiveResponse(requestId, null);
}
}
async _handleDeleteResponse(response: Response): Promise<void> {
@@ -853,6 +853,7 @@ class Wrapper extends React.PureComponent<Props, State> {
editorIndentSize={settings.editorIndentSize}
editorKeyMap={settings.editorKeyMap}
editorLineWrapping={settings.editorLineWrapping}
environment={activeEnvironment}
filter={responseFilter}
filterHistory={responseFilterHistory}
handleDeleteResponse={this._handleDeleteResponse}
@@ -709,6 +709,7 @@ class App extends PureComponent {
}
async _handleSetActiveResponse(requestId, activeResponse = null) {
const { activeEnvironment } = this.props;
const activeResponseId = activeResponse ? activeResponse._id : null;
await App._updateRequestMetaByParentId(requestId, { activeResponseId });
@@ -716,7 +717,8 @@ class App extends PureComponent {
if (activeResponseId) {
response = await models.response.getById(activeResponseId);
} else {
response = await models.response.getLatestForRequest(requestId);
const environmentId = activeEnvironment ? activeEnvironment._id : null;
response = await models.response.getLatestForRequest(requestId, environmentId);
}
const requestVersionId = response ? response.requestVersionId : 'n/a';
@@ -269,10 +269,19 @@ export const selectActiveRequestMeta = createSelector(
export const selectActiveRequestResponses = createSelector(
selectActiveRequest,
selectEntitiesLists,
(activeRequest, entities) => {
selectActiveWorkspaceMeta,
(activeRequest, entities, meta) => {
const requestId = activeRequest ? activeRequest._id : 'n/a';
return entities.responses
.filter(response => requestId === response.parentId)
.filter(response => {
const requestMatches = requestId === response.parentId;
const environmentMatches = response.environmentId === meta.activeEnvironmentId;
// Legacy responses were sent before environment scoping existed
const isLegacy = response.environmentId === '__LEGACY__';
return requestMatches && (environmentMatches || isLegacy);
})
.sort((a, b) => (a.created > b.created ? -1 : 1));
},
);
@@ -533,6 +533,9 @@ function _genTestContext(requests, responses, extraInfoRoot) {
return {
renderPurpose: 'send',
context: {
getEnvironmentId() {
return null;
},
getExtraInfo(key) {
if (_extraInfo) {
return _extraInfo[key] || null;
+16 -4
View File
@@ -2,6 +2,10 @@ const jq = require('jsonpath');
const iconv = require('iconv-lite');
const { query: queryXPath } = require('insomnia-xpath');
function isFilterableField(field) {
return field !== 'raw' && field !== 'url';
}
module.exports.templateTags = [
{
name: 'response',
@@ -27,6 +31,11 @@ module.exports.templateTags = [
description: 'value of response header',
value: 'header',
},
{
displayName: 'Request URL',
description: 'Url of initiating request',
value: 'url',
},
],
},
{
@@ -37,7 +46,7 @@ module.exports.templateTags = [
{
type: 'string',
encoding: 'base64',
hide: args => args[0].value === 'raw',
hide: args => !isFilterableField(args[0].value),
displayName: args => {
switch (args[0].value) {
case 'body':
@@ -77,7 +86,7 @@ module.exports.templateTags = [
filter = filter || '';
resendBehavior = (resendBehavior || 'never').toLowerCase();
if (!['body', 'header', 'raw'].includes(field)) {
if (!['body', 'header', 'raw', 'url'].includes(field)) {
throw new Error(`Invalid response field ${field}`);
}
@@ -90,7 +99,8 @@ module.exports.templateTags = [
throw new Error(`Could not find request ${id}`);
}
let response = await context.util.models.response.getLatestForRequestId(id);
const environmentId = context.context.getEnvironmentId();
let response = await context.util.models.response.getLatestForRequestId(id, environmentId);
let shouldResend = false;
if (context.context.getExtraInfo('fromResponseTag')) {
@@ -132,7 +142,7 @@ module.exports.templateTags = [
throw new Error('No successful responses for request');
}
if (field !== 'raw' && !filter) {
if (isFilterableField(field) && !filter) {
throw new Error(`No ${field} filter specified`);
}
@@ -140,6 +150,8 @@ module.exports.templateTags = [
if (field === 'header') {
return matchHeader(response.headers, sanitizedFilter);
} else if (field === 'url') {
return response.url;
} else if (field === 'raw') {
const bodyBuffer = context.util.models.response.getBodyBuffer(response, '');
const match = response.contentType.match(/charset=([\w-]+)/);