Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module.exports = {
lines: 64,
},
'packages/respect-core/': {
statements: 84,
statements: 83,
branches: 74,
functions: 84,
lines: 84,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('getServerUrl', () => {
} as unknown as TestContext;
const descriptionName = 'test';
const result = getServerUrl({ ctx, descriptionName });
expect(result).toEqual({ url: 'https://example.com' });
expect(result).toEqual({ url: 'https://example.com', parameters: [] });
});

it('should return undefined when path does not include url and no servers provided', () => {
Expand All @@ -38,7 +38,7 @@ describe('getServerUrl', () => {
} as unknown as TestContext;
const descriptionName = 'test';
const result = getServerUrl({ ctx, descriptionName });
expect(result).toEqual({ url: 'https://example.com' });
expect(result).toEqual({ url: 'https://example.com', parameters: [] });
});

it('should return server url from sourceDescription x-serverUrl resolved from context', () => {
Expand All @@ -56,7 +56,7 @@ describe('getServerUrl', () => {
} as unknown as TestContext;
const descriptionName = 'test';
const result = getServerUrl({ ctx, descriptionName });
expect(result).toEqual({ url: 'https://example.com' });
expect(result).toEqual({ url: 'https://example.com', parameters: [] });
});

it('should return overwritten server url from sourceDescription x-serverUrl resolved from context', () => {
Expand Down Expand Up @@ -97,7 +97,7 @@ describe('getServerUrl', () => {
} as unknown as OperationDetails & { servers: { url: string }[] };
const descriptionName = 'test';
const result = getServerUrl({ ctx, descriptionName, openapiOperation });
expect(result).toEqual({ url: 'https://example1.com' });
expect(result).toEqual({ url: 'https://example1.com', parameters: [] });
});

it('should return "x-operation" url as server url when descriptionName is not provided', () => {
Expand Down Expand Up @@ -156,7 +156,7 @@ describe('getServerUrl', () => {
} as unknown as TestContext;

const result = getServerUrl({ ctx, descriptionName: '' });
expect(result).toEqual({ url: 'https://api.example.com' });
expect(result).toEqual({ url: 'https://api.example.com', parameters: [] });
});

it('should return undefined when neither x-serverUrl nor $sourceDescriptions server is available when descriptionName is not provided and there is only one sourceDescription', () => {
Expand Down Expand Up @@ -211,7 +211,7 @@ describe('getServerUrl', () => {
openapiOperation: mockDescriptionOperation,
} as unknown as GetServerUrlInput);

expect(result).toEqual({ url: 'https://server1.com' });
expect(result).toEqual({ url: 'https://server1.com', parameters: [] });
});

it('should return undefined when descriptionOperation.servers is empty', () => {
Expand Down Expand Up @@ -285,4 +285,67 @@ describe('getServerUrl', () => {
const result = getServerUrl({ ctx, descriptionName });
expect(result).toEqual({ url: 'https://cli.com' });
});

it('should return server url from openapi server with variables', () => {
const ctx = {
$sourceDescriptions: {
test: {
paths: {
'/test': {
get: {
servers: [
{
url: 'https://{region}@{task}.{region}.openapi-server-with-vars.{domain}/v1?param={task}#fragment,param={task}',
variables: {
domain: {
default: 'com',
enum: ['com', 'net'],
},
region: {
default: 'us',
enum: ['us', 'eu', 'asia'],
},
task: {
default: 'ping',
},
},
},
],
},
},
},
},
},
} as unknown as TestContext;
const openapiOperation = {
servers: [
{
url: 'https://{region}@{task}.{region}.openapi-server-with-vars.{domain}/v1?param={task}#fragment,param={task}',
variables: {
domain: {
default: 'com',
enum: ['com', 'net'],
},
region: {
default: 'us',
enum: ['us', 'eu', 'asia'],
},
task: {
default: 'ping',
},
},
},
],
} as unknown as OperationDetails;
const descriptionName = 'test';
const result = getServerUrl({ ctx, descriptionName, openapiOperation });
expect(result).toEqual({
url: 'https://{region}@{task}.{region}.openapi-server-with-vars.{domain}/v1?param={task}#fragment,param={task}',
parameters: [
{ name: 'domain', value: 'com', in: 'path' },
{ name: 'region', value: 'us', in: 'path' },
{ name: 'task', value: 'ping', in: 'path' },
],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ describe('prepareRequest', () => {
summary: 'Get a list of breeds',
tags: ['Breeds'],
});
expect(serverUrl).toEqual({ url: 'https://catfact.ninja/' });
expect(serverUrl).toEqual({ url: 'https://catfact.ninja/', parameters: [] });
expect(requestBody).toEqual(undefined);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface OpenApiRequestData {
requestBody?: Record<string, unknown>;
contentType?: string;
parameters: ParameterWithIn[];
contentTypeParameters: ParameterWithIn[];
}

export function getRequestDataFromOpenApi(
Expand All @@ -30,13 +31,15 @@ export function getRequestDataFromOpenApi(
const accept = getAcceptHeader(operation);
const parameters = getUniqueParameters([
...transformParameters(operation.pathParameters),
{ name: 'content-type', in: 'header' as const, value: contentType },
...(accept ? [{ name: 'accept', in: 'header' as const, value: accept }] : []),
...transformParameters(operation.parameters),
]).filter(({ value }) => value);

return {
parameters,
contentTypeParameters: [
...(contentType ? [{ name: 'content-type', in: 'header' as const, value: contentType }] : []),
...(accept ? [{ name: 'accept', in: 'header' as const, value: accept }] : []),
],
requestBody,
contentType,
};
Expand Down
54 changes: 44 additions & 10 deletions packages/respect-core/src/modules/flow-runner/get-server-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ export type GetServerUrlInput = {
xOperation?: ExtendedOperation;
};

export type ServerObject = {
url: string;
description?: string;
variables?: Record<
string,
{
default: string;
enum?: string[];
description?: string;
}
>;
};

export function getServerUrl({
ctx,
descriptionName,
Expand Down Expand Up @@ -42,9 +55,9 @@ export function getServerUrl({
};
}

return {
url: getValueFromContext('$' + `sourceDescriptions.${descriptionName}.servers.0.url`, ctx),
};
return resolveOpenApiServerUrlWithVariables(
getValueFromContext('$' + `sourceDescriptions.${descriptionName}.servers.0`, ctx)
);
}

if (openapiOperation?.servers?.[0]) {
Expand All @@ -65,20 +78,23 @@ export function getServerUrl({
: undefined;
}

return serverUrlOverride ? { url: serverUrlOverride } : openapiOperation.servers[0];
return serverUrlOverride
? { url: serverUrlOverride }
: resolveOpenApiServerUrlWithVariables(openapiOperation.servers[0]);
}

if (!descriptionName && ctx?.sourceDescriptions && ctx.sourceDescriptions.length === 1) {
const sourceDescription = ctx.sourceDescriptions[0];

let serverUrl = '';
if ('x-serverUrl' in sourceDescription && sourceDescription['x-serverUrl']) {
serverUrl = sourceDescription['x-serverUrl'];
return { url: sourceDescription['x-serverUrl'] };
} else {
serverUrl = ctx.$sourceDescriptions[sourceDescription.name]?.servers[0]?.url || undefined;
return (
resolveOpenApiServerUrlWithVariables(
ctx.$sourceDescriptions[sourceDescription.name]?.servers[0]
) || undefined
);
}

return serverUrl ? { url: serverUrl } : undefined;
}

if (
Expand All @@ -91,5 +107,23 @@ export function getServerUrl({
}

// Get first available server url from the description
return ctx.$sourceDescriptions[descriptionName].servers[0];
return resolveOpenApiServerUrlWithVariables(
ctx.$sourceDescriptions[descriptionName].servers?.[0]
);
}

function resolveOpenApiServerUrlWithVariables(server?: ServerObject) {
if (!server) {
return undefined;
}
return {
url: server.url,
parameters: Object.entries(server.variables || {})
.map(([key, value]) => ({
in: 'path',
name: key,
value: value.default,
}))
.filter(({ value }) => !!value),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export async function prepareRequest(

let path = '';
let method;
const serverUrl: { url: string; descriptionName?: string } | undefined = getServerUrl({

const serverUrl: { url: string; parameters?: ParameterWithIn[] } | undefined = getServerUrl({
ctx,
descriptionName: openapiOperation?.descriptionName,
openapiOperation,
Expand Down Expand Up @@ -89,7 +90,10 @@ export async function prepareRequest(
typeof requestBody === 'object'
? [{ in: 'header', name: 'content-type', value: 'application/json' }]
: [],
requestDataFromOpenAPI?.parameters || [],
serverUrl?.parameters || [],
requestDataFromOpenAPI?.contentTypeParameters || [],
// if step.parameters is defined, we do not auto-populate parameters from the openapi operation
step.parameters ? [] : requestDataFromOpenAPI?.parameters || [],
resolveParameters(workflowLevelParameters, ctx),
stepRequestBodyContentType
? [{ in: 'header', name: 'content-type', value: stepRequestBodyContentType }]
Expand Down
26 changes: 7 additions & 19 deletions packages/respect-core/src/utils/api-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,16 @@ export class ApiFetcher implements IFetcher {
.join('; ');
}

let resolvedPath = resolvePath(path, pathParams) || '';
const resolvedPath = resolvePath(path, pathParams) || '';
const pathWithSearchParams = `${resolvedPath}${
searchParams.toString() ? '?' + searchParams.toString() : ''
}`;
const pathToFetch = `${trimTrailingSlash(serverUrl.url)}${pathWithSearchParams}`;
const resolvedServerUrl = resolvePath(serverUrl.url, pathParams) || '';
const urlToFetch = `${trimTrailingSlash(resolvedServerUrl)}${pathWithSearchParams}`;

if (pathToFetch.startsWith('/')) {
if (urlToFetch.startsWith('/')) {
logger.error(
bgRed(` Wrong url: ${inverse(pathToFetch)} `) +
bgRed(` Wrong url: ${inverse(urlToFetch)} `) +
` Did you forget to provide a correct "serverUrl"?\n`
);
}
Expand Down Expand Up @@ -223,27 +224,16 @@ export class ApiFetcher implements IFetcher {
// Start of the verbose logs
this.initVerboseLogs({
headerParams: maskedHeaderParams,
host: serverUrl.url,
host: resolvedServerUrl,
method: (method || 'get').toUpperCase() as OperationMethod,
path: maskedPathParams || '',
body: maskedBody,
});

const wrappedFetch = this.harLogs ? withHar(this.fetch, { har: this.harLogs }) : fetch;
// Resolve pathToFetch with pathParams for the second time in order
// to handle described servers->variables in the OpenAPI spec.
// E.G.:
// servers:
// - url: 'https://api-sandbox.redocly.com/organizations/{organizationId}'
// TODO: remove/update after the support of the described servers->variables in the Arazzo spec.
resolvedPath = resolvePath(pathToFetch, pathParams) || '';
if (!resolvedPath) {
throw new Error('Path to fetch is undefined');
}

const startTime = performance.now();

const result = await wrappedFetch(resolvedPath, {
const result = await wrappedFetch(urlToFetch, {
method: (method || 'get').toUpperCase() as OperationMethod,
headers,
...(!isEmpty(requestBody) && {
Expand Down Expand Up @@ -297,8 +287,6 @@ export class ApiFetcher implements IFetcher {
time: responseTime,
header: Object.fromEntries(result.headers?.entries() || []),
contentType: responseContentType,
query: Object.fromEntries(searchParams.entries()),
path: pathParams,
};
};
}
Loading