Files
nimbus/resources/js/utils/request/curl-generator.ts
Mazen Touati e1b844cee0 feat(history): add history viewer and rewind (#38)
* feat(ui): add `input group` base component

* feat(history): add history viewer and rewind

* test: update selector snapshot

* test: add PW base page

* style: apply TS style fixes

* chore(history): request history wiki

* chore(history): remove unwanted symbol

* chore: fix type

* style: apply TS style fixes
2026-01-17 20:50:00 +01:00

291 lines
7.8 KiB
TypeScript

import { ParameterContract } from '@/interfaces';
import { AuthorizationContract } from '@/interfaces/auth/authorization';
import { AuthorizationType } from '@/interfaces/generated';
import { PendingRequest, RequestBodyTypeEnum } from '@/interfaces/http';
import { ParameterType } from '@/interfaces/ui/key-value-parameters';
import { buildRequestUrl } from '@/utils';
import { getMimeTypeForPayloadType } from '@/utils/request/content-type-header-generator';
/**
* Result of cURL command generation.
*/
export interface CurlGenerationResult {
command: string;
hasSpecialAuth: boolean;
}
/**
* Generates a complete cURL command from a pending request.
*
* Builds a properly formatted cURL command with method, URL, headers,
* authorization, and body data.
*/
export function generateCurlCommand(
request: PendingRequest,
baseUrl: string,
): CurlGenerationResult {
const { queryParameters, requestBody } =
getEffectiveQueryParametersAndBodyValue(request);
const methodPart = buildHttpMethodPart(request.method);
const fullUrl = buildRequestUrl(baseUrl, request.endpoint, queryParameters);
const headerParts = buildRequestHeaderParts(request);
const authPart = buildAuthorizationHeaderPart(request.authorization);
const bodyParts = buildRequestBodyParts(requestBody);
const command = ['curl']
.concat(methodPart ? [methodPart] : [])
.concat([`"${fullUrl}"`])
.concat(headerParts)
.concat(authPart ? [authPart] : [])
.concat(bodyParts)
.join(' \\\n ');
return {
command: command,
hasSpecialAuth: requiresSpecialAuthorization(request.authorization),
};
}
function getEffectiveQueryParametersAndBodyValue(request: PendingRequest): {
queryParameters: ParameterContract[];
requestBody: FormData | string | null;
} {
const requestBody = getRequestEffectiveBody(request);
const isGetRequest = request.method.toLowerCase() === 'get';
// In GET requests, we want to move the body content to query parameters and discard the body.
if (isGetRequest) {
const requestBodyKeyValuePairs = transformRequestBodyToKeyValuePairs(
requestBody,
request.payloadType,
);
return {
queryParameters: [
...request.queryParameters.filter(isValidParameter),
...convertKeyValuePairsToQueryParameters(requestBodyKeyValuePairs),
],
requestBody: null,
};
}
return {
queryParameters: request.queryParameters,
requestBody: requestBody,
};
}
/**
* Builds HTTP method part.
*/
function buildHttpMethodPart(method: string): string | null {
const upperMethod = method.toUpperCase();
// GET is the default HTTP method in cURL, so no explicit flag needed
if (upperMethod === 'GET') {
return null;
}
return `-X ${upperMethod}`;
}
/**
* Builds request header parts.
*/
function buildRequestHeaderParts(request: PendingRequest): string[] {
const validHeaders = getValidHeaders(request);
const headerParts = validHeaders.map(header => `-H "${header.key}: ${header.value}"`);
// Add Content-Type header for payload types with MIME types if not already present
const mimeType = getMimeTypeForPayloadType(request.payloadType);
if (mimeType) {
const hasContentType = validHeaders.some(
header => header.key.toLowerCase() === 'content-type',
);
if (!hasContentType) {
headerParts.push(`-H "Content-Type: ${mimeType}"`);
}
}
return headerParts;
}
/**
* Builds authorization header part.
*/
function buildAuthorizationHeaderPart(
authorization: AuthorizationContract,
): string | null {
const authHeader = buildAuthHeader(authorization);
if (!authHeader) {
return null;
}
return `-H "${authHeader}"`;
}
function convertKeyValuePairsToQueryParameters(
keyValuePairs: Record<string, string>,
): ParameterContract[] {
return Object.entries(keyValuePairs).map(([key, value]) => ({
type: ParameterType.Text,
enabled: true,
key,
value,
}));
}
/**
* Filters headers to only include valid ones.
*/
function getValidHeaders(request: PendingRequest): ParameterContract[] {
return request.headers.filter(isValidParameter);
}
/**
* Checks if a parameter is valid for inclusion.
*/
function isValidParameter(parameter: ParameterContract): boolean {
return parameter.enabled && parameter.key.trim() !== '';
}
/**
* Builds authorization header string.
*/
function buildAuthHeader(authorization: AuthorizationContract): string | null {
if (!authorization) {
return null;
}
switch (authorization.type) {
case AuthorizationType.Bearer:
return `Authorization: Bearer ${authorization.value}`;
case AuthorizationType.Basic:
return buildBasicAuthHeader(authorization.value);
case AuthorizationType.None:
case AuthorizationType.CurrentUser:
case AuthorizationType.Impersonate:
return null;
default:
return null;
}
}
/**
* Builds Basic authentication header.
*/
function buildBasicAuthHeader(authValue: {
username: string;
password: string;
}): string | null {
// btoa() encodes username:password string to Base64 for HTTP Basic Authentication
const credentials = btoa(`${authValue.username}:${authValue.password}`);
return `Authorization: Basic ${credentials}`;
}
function getRequestEffectiveBody(request: PendingRequest): FormData | string | null {
const bodyData = request.body;
const methodBodies = bodyData[request.method];
if (methodBodies === undefined) {
return null;
}
const body = methodBodies[request.payloadType];
if (body === undefined) {
return null;
}
return body;
}
/**
* Builds request body parts.
*/
function buildRequestBodyParts(requestBody: FormData | string | null): string[] {
if (requestBody === null) {
return [];
}
return convertBodyValueToRequestParts(requestBody);
}
function transformRequestBodyToKeyValuePairs(
bodyValue: string | FormData | null,
payloadType: RequestBodyTypeEnum,
): Record<string, string> {
if (bodyValue === null) {
return {};
}
if (typeof bodyValue === 'string' && payloadType === RequestBodyTypeEnum.JSON) {
return JSON.parse(bodyValue);
}
if (bodyValue instanceof FormData) {
return Object.fromEntries(
Array.from(bodyValue.entries()).map(([key, value]) => [
key,
value instanceof File ? `@${value}` : value,
]),
);
}
return bodyValue.split('\n').reduce<Record<string, string>>(function (
carry,
bodyLine,
) {
const { 0: key, 1: value } = bodyLine.split('=');
carry[key] = value;
return carry as Record<string, string>;
}, {}) as Record<string, string>;
}
/**
* Formats body value based on payload type.
*/
function convertBodyValueToRequestParts(bodyValue: string | FormData): string[] {
if (typeof bodyValue === 'string') {
return [`-d '${bodyValue}'`];
}
return convertFormDataToCUrlFields(bodyValue);
}
/**
* Converts FormData object to cURL field strings.
*/
function convertFormDataToCUrlFields(formData: FormData): string[] {
return Array.from(formData.entries()).map(([key, value]) => {
if (value instanceof File) {
return `-F ${key}=@${value.name}`;
}
return `-F ${key}=${value}`;
});
}
/**
* Checks if authorization requires special handling (not standard HTTP headers).
*/
function requiresSpecialAuthorization(authorization: AuthorizationContract): boolean {
if (authorization.type === AuthorizationType.CurrentUser) {
return true;
}
return authorization.type === AuthorizationType.Impersonate;
}