cosmos-explorer/src/Utils/arm/request.ts
Srinath Narayanan ecdc41ada9
Added more selfserve changes (#443)
* exposed baselineValues

* added getOnSaveNotification

* disable UI when onSave is taking place

* added optional polling

* Added portal notifications

* minor edits

* added label for description

* Added correlationids and polling of refresh

* added label tooltip

* removed ClassInfo decorator

* Added dynamic decription

* added info and warninf types for description

* promise retry changes

* compile errors fixed

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* added polling on landing on the page

* edits for error display

* added link generation

* addressed PR comments

* modified test

* fixed compilation error
2021-03-09 16:07:23 -08:00

157 lines
4.3 KiB
TypeScript

/*
A general purpose fetch function for ARM resources. Not designed to be used directly
Instead, generate ARM clients that consume this function with stricter typing.
*/
import promiseRetry, { AbortError } from "p-retry";
import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext";
interface ErrorResponse {
code: string;
message: string;
}
// ARM sometimes returns an error wrapped in a top level error object
// Example: 409 Conflict error when trying to delete a locked resource
interface WrappedErrorResponse {
error: ErrorResponse;
}
type ParsedErrorResponse = ErrorResponse | WrappedErrorResponse;
export class ARMError extends Error {
constructor(message: string) {
super(message);
// Set the prototype explicitly.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
Object.setPrototypeOf(this, ARMError.prototype);
}
public code?: string | number;
}
interface ARMQueryParams {
filter?: string;
metricNames?: string;
}
interface Options {
host: string;
path: string;
apiVersion: string;
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
body?: unknown;
queryParams?: ARMQueryParams;
}
export async function armRequestWithoutPolling<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
if (queryParams) {
queryParams.filter && url.searchParams.append("$filter", queryParams.filter);
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
}
if (!userContext.authorizationToken) {
throw new Error("No authority token provided");
}
const response = await window.fetch(url.href, {
method,
headers: {
Authorization: userContext.authorizationToken,
},
body: requestBody ? JSON.stringify(requestBody) : undefined,
});
if (!response.ok) {
let error: ARMError;
try {
const errorResponse = (await response.json()) as ParsedErrorResponse;
if ("error" in errorResponse) {
error = new ARMError(errorResponse.error.message);
error.code = errorResponse.error.code;
} else {
error = new ARMError(errorResponse.message);
error.code = errorResponse.code;
}
} catch (error) {
throw new Error(await response.text());
}
throw error;
}
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
const responseBody = (await response.json()) as T;
return { result: responseBody, operationStatusUrl: operationStatusUrl };
}
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
export async function armRequest<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
}: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
});
const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) {
return await promiseRetry(() => getOperationStatus(operationStatusUrl));
}
return armRequestResult.result;
}
async function getOperationStatus(operationStatusUrl: string) {
if (!userContext.authorizationToken) {
throw new Error("No authority token provided");
}
const response = await window.fetch(operationStatusUrl, {
headers: {
Authorization: userContext.authorizationToken,
},
});
if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse;
const error = new Error(errorResponse.message) as ARMError;
error.code = errorResponse.code;
throw new AbortError(error);
}
if (response.status === 204) {
return;
}
const body = await response.json();
const status = body.status;
if (!status && response.status === 200) {
return body;
}
if (status === "Canceled" || status === "Failed") {
const errorMessage = body.error ? JSON.stringify(body.error) : "Operation could not be completed";
const error = new Error(errorMessage);
throw new AbortError(error);
}
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
}