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

Request path parameters #6994

Merged
merged 12 commits into from Jan 18, 2024
Expand Up @@ -72,6 +72,7 @@ test.describe('Environment Editor', async () => {
await page.getByLabel('Request Collection').getByTestId('New Request').press('Enter');

// Add number variable to request body
await page.getByRole('tab', { name: 'Plain' }).click();
await page.locator('pre').filter({ hasText: '_.exampleObject.anotherNumber' }).click();
await page.getByTestId('CodeEditor').getByRole('textbox').press('Enter');
await page.getByTestId('CodeEditor').getByRole('textbox').press('Control+ ');
Expand Down
4 changes: 4 additions & 0 deletions packages/insomnia-smoke-test/tests/smoke/graphql.test.ts
Expand Up @@ -21,6 +21,7 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => {

// Open the graphql request
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');
await page.getByRole('tab', { name: 'GraphQL' }).click();
// Assert the schema is fetched after switching to GraphQL request
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');

Expand Down Expand Up @@ -62,6 +63,7 @@ test('can render schema and send GraphQL requests with object variables', async

// Open the graphql request
await page.getByLabel('Request Collection').getByTestId('GraphQL request with variables').press('Enter');
await page.getByRole('tab', { name: 'GraphQL' }).click();
// Assert the schema is fetched after switching to GraphQL request
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');

Expand Down Expand Up @@ -103,6 +105,7 @@ test('can render numeric environment', async ({ app, page }) => {

// Open the graphql request
await page.getByLabel('Request Collection').getByTestId('GraphQL request with number').press('Enter');
await page.getByRole('tab', { name: 'GraphQL' }).click();
// Assert the schema is fetched after switching to GraphQL request
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');

Expand Down Expand Up @@ -141,6 +144,7 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');

// Edit and prettify query
await page.getByRole('tab', { name: 'GraphQL' }).click();
await page.locator('pre[role="presentation"]:has-text("bearer")').click();
await page.locator('.app').press('Enter');
await page.locator('text=Prettify GraphQL').click();
Expand Down
Expand Up @@ -6,12 +6,13 @@ test('Request tabs', async ({ page }) => {

await page.getByLabel('Create in collection').click();
await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter');
await page.getByRole('tab', { name: 'Body' }).click();
await page.getByRole('button', { name: 'Body' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
await page.getByRole('tab', { name: 'Auth' }).click();
await page.getByRole('button', { name: 'Auth' }).click();
await page.getByRole('menuitem', { name: 'OAuth 1.0' }).click();
await page.getByRole('tab', { name: 'Query' }).click();
await page.getByRole('tab', { name: 'Parameters' }).click();
await page.getByRole('tab', { name: 'Headers' }).click();
await page.getByRole('tab', { name: 'Docs' }).click();
await page.locator('text=Add Description').click();
Expand All @@ -26,9 +27,10 @@ test('WS tabs', async ({ page }) => {
await page.getByLabel('Create in collection').click();
await page.getByRole('menuitemradio', { name: 'WebSocket Request' }).click();
await page.getByRole('tab', { name: 'JSON' }).click();
await page.getByLabel('Websocket request pane tabs').getByRole('button', { name: 'JSON' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
await page.getByRole('tab', { name: 'Auth' }).click();
await page.getByRole('tab', { name: 'Query' }).click();
await page.getByRole('tab', { name: 'Parameters' }).click();
await page.getByRole('tab', { name: 'Headers' }).click();
await page.getByRole('tab', { name: 'Docs' }).click();
await page.getByRole('button', { name: 'Add Description' }).click();
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/common/__tests__/database.test.ts
Expand Up @@ -200,7 +200,7 @@ describe('requestCreate()', () => {
parentId: 'wrk_123',
};
const r = await models.request.create(patch);
expect(Object.keys(r).length).toBe(21);
expect(Object.keys(r).length).toBe(22);
expect(r._id).toMatch(/^req_[a-zA-Z0-9]{32}$/);
expect(r.created).toBeGreaterThanOrEqual(now);
expect(r.modified).toBeGreaterThanOrEqual(now);
Expand Down
22 changes: 21 additions & 1 deletion packages/insomnia/src/common/render.ts
Expand Up @@ -6,7 +6,7 @@ import type { CookieJar } from '../models/cookie-jar';
import type { Environment } from '../models/environment';
import type { GrpcRequest, GrpcRequestBody } from '../models/grpc-request';
import { isProject, Project } from '../models/project';
import type { Request } from '../models/request';
import { PATH_PARAMETER_REGEX, type Request } from '../models/request';
import { isRequestGroup, RequestGroup } from '../models/request-group';
import { WebSocketRequest } from '../models/websocket-request';
import { isWorkspace, Workspace } from '../models/workspace';
Expand Down Expand Up @@ -493,6 +493,7 @@ export async function getRenderedRequestAndContext(
renderContext,
request.settingDisableRenderRequestBody ? /^body.*/ : null,
);

const renderedRequest = renderResult._request;
const renderedCookieJar = renderResult._cookieJar;
renderedRequest.description = await render(description, renderContext, null, KEEP_ON_ERROR);
Expand All @@ -514,6 +515,24 @@ export async function getRenderedRequestAndContext(

// Default the proto if it doesn't exist
renderedRequest.url = setDefaultProtocol(renderedRequest.url);

// Render path parameters
if (renderedRequest.pathParameters) {
// Replace path parameters in URL with their rendered values
// Path parameters are path segments that start with a colon, e.g. :id
renderedRequest.url = renderedRequest.url.replace(PATH_PARAMETER_REGEX, match => {
const paramName = match.replace('\/:', '');
const param = renderedRequest.pathParameters?.find(p => p.name === paramName);

if (param && param.value) {
// The parameter value needs to be URL encoded
return `/${encodeURIComponent(param.value)}`;
}

return match;
});
}

return {
context: renderContext,
request: {
Expand All @@ -523,6 +542,7 @@ export async function getRenderedRequestAndContext(
isPrivate: false,
_id: renderedRequest._id,
authentication: renderedRequest.authentication,
pathParameters: renderedRequest.pathParameters,
body: renderedRequest.body,
created: renderedRequest.created,
modified: renderedRequest.modified,
Expand Down
3 changes: 3 additions & 0 deletions packages/insomnia/src/models/__tests__/request.test.ts
Expand Up @@ -20,6 +20,7 @@ describe('init()', () => {
name: 'New Request',
description: '',
parameters: [],
pathParameters: [],
url: '',
settingStoreCookies: true,
settingSendCookies: true,
Expand Down Expand Up @@ -56,6 +57,7 @@ describe('create()', () => {
method: 'GET',
name: 'Test Request',
parameters: [],
pathParameters: [],
url: '',
settingStoreCookies: true,
settingSendCookies: true,
Expand Down Expand Up @@ -388,6 +390,7 @@ describe('migrate()', () => {
headers: [],
authentication: {},
parameters: [],
pathParameters: [],
parentId: null,
body: {
mimeType: '',
Expand Down
46 changes: 46 additions & 0 deletions packages/insomnia/src/models/request.ts
Expand Up @@ -82,6 +82,50 @@ export interface RequestBodyParameter {
type?: string;
}

export interface RequestPathParameter {
name: string;
value: string;
}

export const PATH_PARAMETER_REGEX = /\/:[^/?#]+/g;

export const getPathParametersFromUrl = (url: string): string[] => {
// Find all path parameters in the URL. Path parameters are defined as segments of the URL that start with a colon.
const urlPathParameters = url.match(PATH_PARAMETER_REGEX)?.map(String).map(match => match.replace('\/:', '')) || [];
const uniqueUrlPathParameters = [...new Set(urlPathParameters)];

return uniqueUrlPathParameters;
};

export const getCombinedPathParametersFromUrl = (url: string, pathParameters: RequestPathParameter[]): RequestPathParameter[] => {
// Extract path parameters from the URL
const urlPathParameters = getPathParametersFromUrl(url);

// Initialize an empty array for saved path parameters
let savedPathParameters: RequestPathParameter[] = [];

// Check if there are any path parameters in the active request
if (pathParameters) {
// Filter out the saved path parameters
savedPathParameters = pathParameters.filter(p => urlPathParameters.includes(p.name));
}

// Initialize an empty set for unsaved URL path parameters
let unsavedUrlPathParameters = new Set<RequestPathParameter>();

// Check if there are any path parameters in the URL
if (urlPathParameters) {
// Filter out the unsaved URL path parameters
unsavedUrlPathParameters = new Set(
urlPathParameters.filter(p => !savedPathParameters.map(p => p.name).includes(p))
.map(p => ({ name: p, value: '' }))
);
}

// Combine the saved and unsaved path parameters
return [...savedPathParameters, ...unsavedUrlPathParameters];
};

export interface RequestBody {
mimeType?: string | null;
text?: string;
Expand All @@ -96,6 +140,7 @@ export interface BaseRequest {
method: string;
body: RequestBody;
parameters: RequestParameter[];
pathParameters: RequestPathParameter[];
headers: RequestHeader[];
authentication: RequestAuthentication;
metaSortKey: number;
Expand Down Expand Up @@ -135,6 +180,7 @@ export function init(): BaseRequest {
authentication: {},
metaSortKey: -1 * Date.now(),
isPrivate: false,
pathParameters: [],
// Settings
settingStoreCookies: true,
settingSendCookies: true,
Expand Down
4 changes: 3 additions & 1 deletion packages/insomnia/src/models/websocket-request.ts
@@ -1,6 +1,6 @@
import { database } from '../common/database';
import type { BaseModel } from '.';
import { RequestAuthentication, RequestHeader, RequestParameter } from './request';
import { RequestAuthentication, RequestHeader, RequestParameter, RequestPathParameter } from './request';

export const name = 'WebSocket Request';

Expand All @@ -20,6 +20,7 @@ export interface BaseWebSocketRequest {
headers: RequestHeader[];
authentication: RequestAuthentication;
parameters: RequestParameter[];
pathParameters: RequestPathParameter[];
settingEncodeUrl: boolean;
settingStoreCookies: boolean;
settingSendCookies: boolean;
Expand All @@ -43,6 +44,7 @@ export const init = (): BaseWebSocketRequest => ({
headers: [],
authentication: {},
parameters: [],
pathParameters: [],
settingEncodeUrl: true,
settingStoreCookies: true,
settingSendCookies: true,
Expand Down
Expand Up @@ -195,6 +195,7 @@ describe('app.export.*', () => {
modified: 222,
name: 'New Request',
parameters: [],
pathParameters: [],
parentId: 'wrk_1',
settingDisableRenderRequestBody: false,
settingEncodeUrl: true,
Expand Down
31 changes: 0 additions & 31 deletions packages/insomnia/src/ui/components/editors/query-editor.tsx

This file was deleted.