Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate post-request script to the engine - INS-3785,INS-3786 #7329

Merged
merged 10 commits into from May 15, 2024
10 changes: 9 additions & 1 deletion packages/insomnia-sdk/src/objects/insomnia.ts
Expand Up @@ -11,6 +11,7 @@ import { unsupportedError } from './properties';
import { Request as ScriptRequest, RequestOptions, toScriptRequestBody } from './request';
import { RequestInfo } from './request-info';
import { Response as ScriptResponse } from './response';
import { toScriptResponse } from './response';
import { sendRequest } from './send-request';
import { test } from './test';
import { toUrlObject } from './urls';
Expand All @@ -23,6 +24,7 @@ export class InsomniaObject {
public request: ScriptRequest;
public cookies: CookieObject;
public info: RequestInfo;
public response?: ScriptResponse;

private clientCertificates: ClientCertificate[];
private _expect = expect;
Expand All @@ -47,6 +49,7 @@ export class InsomniaObject {
clientCertificates: ClientCertificate[];
cookies: CookieObject;
requestInfo: RequestInfo;
response?: ScriptResponse;
},
log: (...msgs: any[]) => void,
) {
Expand All @@ -57,6 +60,7 @@ export class InsomniaObject {
this._iterationData = rawObj.iterationData;
this.variables = rawObj.variables;
this.cookies = rawObj.cookies;
this.response = rawObj.response;

this.info = rawObj.requestInfo;
this.request = rawObj.request;
Expand Down Expand Up @@ -108,11 +112,12 @@ export class InsomniaObject {
clientCertificates: this.clientCertificates,
cookieJar: this.cookies.jar().toInsomniaCookieJar(),
info: this.info.toObject(),
response: this.response ? this.response.toObject() : undefined,
};
};
}

export function initInsomniaObject(
export async function initInsomniaObject(
rawObj: RequestContext,
log: (...args: any[]) => void,
) {
Expand Down Expand Up @@ -206,6 +211,8 @@ export function initInsomniaObject(
};
const request = new ScriptRequest(reqOpt);

const response = rawObj.response ? await toScriptResponse(request, rawObj.response) : undefined;

return new InsomniaObject(
{
globals,
Expand All @@ -218,6 +225,7 @@ export function initInsomniaObject(
clientCertificates: rawObj.clientCertificates,
cookies,
requestInfo,
response,
},
log,
);
Expand Down
3 changes: 3 additions & 0 deletions packages/insomnia-sdk/src/objects/interfaces.ts
Expand Up @@ -2,6 +2,7 @@ import { CookieJar as InsomniaCookieJar } from 'insomnia/src//models/cookie-jar'
import { ClientCertificate } from 'insomnia/src/models/client-certificate';
import type { Request } from 'insomnia/src/models/request';
import { Settings } from 'insomnia/src/models/settings';
import { sendCurlAndWriteTimeline } from 'insomnia/src/network/network';

export interface RequestContext {
request: Request;
Expand All @@ -17,4 +18,6 @@ export interface RequestContext {
settings: Settings;
clientCertificates: ClientCertificate[];
cookieJar: InsomniaCookieJar;
// only for the post-request script
response?: Awaited<ReturnType<typeof sendCurlAndWriteTimeline>>;
}
70 changes: 70 additions & 0 deletions packages/insomnia-sdk/src/objects/response.ts
@@ -1,4 +1,6 @@
import { RESPONSE_CODE_REASONS } from 'insomnia/src/common/constants';
import { Compression, ResponseHeader } from 'insomnia/src/models/response';
import { sendCurlAndWriteTimeline } from 'insomnia/src/network/network';

import { Cookie, CookieOptions } from './cookies';
import { CookieList } from './cookies';
Expand Down Expand Up @@ -183,3 +185,71 @@ export class Response extends Property {
return this.body.toString();
}
}

export async function toScriptResponse(
originalRequest: Request,
partialInsoResponse: Awaited<ReturnType<typeof sendCurlAndWriteTimeline>>,
): Promise<Response | undefined> {
if (partialInsoResponse.error) {
// response basically doesn't contain anything
return undefined;
}

// TODO: improve the type from sendCurlAndWriteTimeline a bit
ihexxa marked this conversation as resolved.
Show resolved Hide resolved
// so that typing in downstream logic could be improved
const partialResponse = partialInsoResponse as {
headers: ResponseHeader[];
bodyPath: string;
bodyCompression: Compression;
statusCode: number;
elapsedTime: number;
statusMessage: string;
};

const headers = partialResponse.headers.map(
insoHeader => ({
key: insoHeader.name,
value: insoHeader.value,
}),
{},
);

const insoCookieOptions = partialResponse.headers
.filter(
header => {
return header.name.toLowerCase() === 'set-cookie';
},
{},
).map(
setCookieHeader => Cookie.parse(setCookieHeader.value)
);

// TODO: handle default path
let responseBody = '';
if (partialResponse.bodyPath) {
const readResponseResult = await window.bridge.readCurlResponse({
bodyPath: partialResponse.bodyPath,
bodyCompression: partialResponse.bodyCompression,
});

if (readResponseResult.error) {
throw Error(`Failed to read body: ${readResponseResult.error}`);
} else {
responseBody = readResponseResult.body;
}
}
ihexxa marked this conversation as resolved.
Show resolved Hide resolved

const responseOption = {
code: partialResponse.statusCode,
// reason is not provided
header: headers,
cookie: insoCookieOptions,
body: responseBody,
// stream is duplicated with body
responseTime: partialResponse.elapsedTime,
status: partialResponse.statusMessage,
originalRequest,
};

return new Response(responseOption);
};
6 changes: 3 additions & 3 deletions packages/insomnia/src/hidden-window-preload.ts
Expand Up @@ -34,7 +34,7 @@ export interface HiddenBrowserWindowToMainBridgeAPI {
curlRequest: (options: any) => Promise<any>;
readCurlResponse: (options: { bodyPath: string; bodyCompression: Compression }) => Promise<{ body: string; error: string }>;
setBusy: (busy: boolean) => void;
writeFile: (logPath: string, logContent: string) => Promise<void>;
appendFile: (logPath: string, logContent: string) => Promise<void>;
asyncTasksAllSettled: () => Promise<void>;
resetAsyncTasks: () => void;
stopMonitorAsyncTasks: () => void;
Expand Down Expand Up @@ -115,8 +115,8 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = {
setBusy: busy => ipcRenderer.send('set-hidden-window-busy-status', busy),
// TODO: following methods are for simulating current behavior of running async tasks
// in the future, it should be better to keep standard way of handling async tasks to avoid confusion
writeFile: (logPath: string, logContent: string) => {
return fs.promises.writeFile(logPath, logContent);
appendFile: (logPath: string, logContent: string) => {
return fs.promises.appendFile(logPath, logContent);
},
Promise: OriginalPromise,
asyncTasksAllSettled,
Expand Down
4 changes: 2 additions & 2 deletions packages/insomnia/src/hidden-window.ts
Expand Up @@ -34,7 +34,7 @@ const runPreRequestScript = async (
console.log(script);
const scriptConsole = new Console();

const executionContext = initInsomniaObject(context, scriptConsole.log);
const executionContext = await initInsomniaObject(context, scriptConsole.log);

const evalInterceptor = (script: string) => {
invariant(script && typeof script === 'string', 'eval is called with invalid or empty value');
Expand Down Expand Up @@ -83,7 +83,7 @@ const runPreRequestScript = async (
const updatedCertificates = mergeClientCertificates(context.clientCertificates, mutatedContextObject.request);
const updatedCookieJar = mergeCookieJar(context.cookieJar, mutatedContextObject.cookieJar);

await window.bridge.writeFile(context.timelinePath, scriptConsole.dumpLogs());
await window.bridge.appendFile(context.timelinePath, scriptConsole.dumpLogs());

console.log('mutatedInsomniaObject', mutatedContextObject);
console.log('context', context);
Expand Down
11 changes: 9 additions & 2 deletions packages/insomnia/src/network/network.ts
Expand Up @@ -74,13 +74,15 @@ export const fetchRequestData = async (requestId: string) => {
};

export const tryToExecutePreRequestScript = async (
ihexxa marked this conversation as resolved.
Show resolved Hide resolved
isPreRequest: boolean,
request: Request,
environment: Environment,
timelinePath: string,
responseId: string,
baseEnvironment: Environment,
clientCertificates: ClientCertificate[],
cookieJar: CookieJar,
response?: Awaited<ReturnType<typeof sendCurlAndWriteTimeline>>,
) => {
if (!request.preRequestScript) {
return {
Expand All @@ -101,7 +103,7 @@ export const tryToExecutePreRequestScript = async (
}, timeout + 1000);
});
const preRequestPromise = cancellableRunPreRequestScript({
ihexxa marked this conversation as resolved.
Show resolved Hide resolved
script: request.preRequestScript,
script: isPreRequest ? request.preRequestScript : request.postRequestScript,
context: {
request,
timelinePath,
Expand All @@ -115,6 +117,7 @@ export const tryToExecutePreRequestScript = async (
clientCertificates,
settings,
cookieJar,
response,
},
});
const output = await Promise.race([timeoutPromise, preRequestPromise]) as {
Expand Down Expand Up @@ -152,7 +155,10 @@ export const tryToExecutePreRequestScript = async (
cookieJar: output.cookieJar,
};
} catch (err) {
await fs.promises.appendFile(timelinePath, JSON.stringify({ value: err.message, name: 'Text', timestamp: Date.now() }) + '\n');
await fs.promises.appendFile(
timelinePath,
JSON.stringify({ value: err.message, name: 'Text', timestamp: Date.now() }) + '\n',
);

const requestId = request._id;
const responsePatch = {
Expand Down Expand Up @@ -282,6 +288,7 @@ export async function sendCurlAndWriteTimeline(
...patch,
};
}

export const responseTransform = async (patch: ResponsePatch, environmentId: string | null, renderedRequest: RenderedRequest, context: Record<string, any>) => {
const response: ResponsePatch = {
...patch,
Expand Down
91 changes: 63 additions & 28 deletions packages/insomnia/src/ui/routes/request.tsx
Expand Up @@ -14,6 +14,7 @@ import { ResponsePatch } from '../../main/network/libcurl-promise';
import * as models from '../../models';
import { BaseModel } from '../../models';
import { CookieJar } from '../../models/cookie-jar';
import { Environment } from '../../models/environment';
import { GrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
import { GrpcRequestMeta } from '../../models/grpc-request-meta';
import * as requestOperations from '../../models/helpers/request-operations';
Expand Down Expand Up @@ -381,6 +382,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
try {
const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams;
const mutatedContext = await tryToExecutePreRequestScript(
true,
req,
environment,
timelinePath,
Expand All @@ -394,34 +396,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
// TODO: improve error message?
return null;
} else {
// persist updated cookieJar if needed
if (mutatedContext.cookieJar) {
await models.cookieJar.update(
mutatedContext.cookieJar,
{ cookies: mutatedContext.cookieJar.cookies },
);
}
// when base environment is activated, `mutatedContext.environment` points to it
const isActiveEnvironmentBase = mutatedContext.environment?._id === baseEnvironment._id;
const hasEnvironmentAndIsNotBase = mutatedContext.environment && !isActiveEnvironmentBase;
if (hasEnvironmentAndIsNotBase) {
await models.environment.update(
environment,
{
data: mutatedContext.environment.data,
dataPropertyOrder: mutatedContext.environment.dataPropertyOrder,
}
);
}
if (mutatedContext.baseEnvironment) {
await models.environment.update(
baseEnvironment,
{
data: mutatedContext.baseEnvironment.data,
dataPropertyOrder: mutatedContext.baseEnvironment.dataPropertyOrder,
}
);
}
await savePatchesMadeByScript(mutatedContext, environment, baseEnvironment);
}

const renderedResult = await tryToInterpolateRequest(
Expand Down Expand Up @@ -461,12 +436,33 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
const responsePatch = await responseTransform(response, activeEnvironmentId, renderedRequest, renderedResult.context);
const is2XXWithBodyPath = responsePatch.statusCode && responsePatch.statusCode >= 200 && responsePatch.statusCode < 300 && responsePatch.bodyPath;
const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath;

const postMutatedContext = await tryToExecutePreRequestScript(
false,
req,
environment,
timelinePath,
responseId,
baseEnvironment,
clientCertificates,
cookieJar,
response,
);
if (!postMutatedContext?.request) {
// exiy early if there was a problem with the pre-request script
// TODO: improve error message?
return null;
} else {
await savePatchesMadeByScript(postMutatedContext, environment, baseEnvironment);
}

if (!shouldWriteToFile) {
const response = await models.response.create(responsePatch, settings.maxHistoryResponses);
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
// setLoading(false);
return null;
}

if (requestMeta.downloadPath) {
const header = getContentDispositionHeader(responsePatch.headers || []);
const name = header
Expand Down Expand Up @@ -500,6 +496,45 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
}
};

async function savePatchesMadeByScript(
mutatedContext: Awaited<ReturnType<typeof tryToExecutePreRequestScript>>,
environment: Environment,
baseEnvironment: Environment,
) {
if (!mutatedContext) {
return;
}

// persist updated cookieJar if needed
if (mutatedContext.cookieJar) {
await models.cookieJar.update(
mutatedContext.cookieJar,
{ cookies: mutatedContext.cookieJar.cookies },
);
}
// when base environment is activated, `mutatedContext.environment` points to it
const isActiveEnvironmentBase = mutatedContext.environment?._id === baseEnvironment._id;
const hasEnvironmentAndIsNotBase = mutatedContext.environment && !isActiveEnvironmentBase;
if (hasEnvironmentAndIsNotBase) {
await models.environment.update(
environment,
{
data: mutatedContext.environment.data,
dataPropertyOrder: mutatedContext.environment.dataPropertyOrder,
}
);
}
if (mutatedContext.baseEnvironment) {
await models.environment.update(
baseEnvironment,
{
data: mutatedContext.baseEnvironment.data,
dataPropertyOrder: mutatedContext.baseEnvironment.dataPropertyOrder,
}
);
}
jackkav marked this conversation as resolved.
Show resolved Hide resolved
}

export const createAndSendToMockbinAction: ActionFunction = async ({ request }) => {
const patch = await request.json() as Partial<Request>;
invariant(typeof patch.url === 'string', 'URL is required');
Expand Down