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:error handling and region support #138

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions __test__/assetSidebarWidget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("AssetSidebarWidget", () => {
user: {} as any,
currentBranch: "mock_branch",
region: "region",
endpoints: { CMA: "", APP: "",DEVELOPER_HUB:"" },
};

let connection: { sendToParent: (...props: any[]) => any };
Expand Down
1 change: 1 addition & 0 deletions __test__/fieldModifierLocation/entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("FieldModifierLocationEntry", () => {
extension_uid: "extension_uid",
installation_uid: "installation_uid",
region: "NA",
endpoints: { CMA: "", APP: "",DEVELOPER_HUB:"" },
stack: {
api_key: "api_key",
created_at: "created_at",
Expand Down
1 change: 1 addition & 0 deletions __test__/organizationFullPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mockData: IOrgFullPageLocationInitData = {
installation_uid: "installation_uid",
extension_uid: "extension_uid",
region: "NA",
endpoints:{CMA:"",APP:"",DEVELOPER_HUB:""},
stack: {} as any,
user: {} as any,
currentBranch: "currentBranch",
Expand Down
1 change: 1 addition & 0 deletions __test__/uiLocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const initData: IAppConfigInitData = {
installation_uid: "installation_uid",
extension_uid: "extension_uid",
region: "NA",
endpoints: { CMA: "https://api.contentstack.io", APP: "https://app.contentstack.app",DEVELOPER_HUB:"" },
stack: mockStackData,
user: {} as any,
currentBranch: "currentBranch",
Expand Down
2 changes: 1 addition & 1 deletion __test__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ describe("formatAppRegion", () => {
});

it("should return unknown for any invalid region", () => {
expect(formatAppRegion("invalid")).toBe(Region.UNKNOWN);
expect(formatAppRegion("invalid")).toBe("invalid");
});
});
335 changes: 82 additions & 253 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GenericObjectType } from "./types/common.types";
import { Entry } from "./types/entry.types";
import { Asset, ContentType, Schema, StackDetail } from "./types/stack.types";
import { OrganizationDetails } from "./types/organization.types";
import { ContentstackEndpoints } from './types/api.type';
import { User } from "./types/user.types";
import Window from "./window";

Expand Down Expand Up @@ -102,6 +103,7 @@ declare interface ICommonInitData {
type: LocationType;
user: User;
manifest?: Manifest;
endpoints: ContentstackEndpoints;
}

export declare interface IOrgFullPageLocationInitData extends ICommonInitData {
Expand Down Expand Up @@ -254,4 +256,7 @@ export enum Region {
AZURE_NA = "AZURE_NA",
AZURE_EU = "AZURE_EU",
GCP_NA = "GCP_NA",
GCP_EU = "GCP_EU",
}

export type RegionType = "UNKNOWN" | "NA" | "EU" | "AZURE_NA" | "AZURE_EU" | "GCP_NA" | string;
13 changes: 10 additions & 3 deletions src/types/api.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { AxiosRequestConfig, AxiosResponse, } from 'axios'
export type RequestConfig = AxiosRequestConfig
export type ProxyResponse = AxiosResponse
import { AxiosRequestConfig, AxiosResponse } from "axios";
export type RequestConfig = AxiosRequestConfig;
export type ProxyResponse = AxiosResponse;

export type ContentstackEndpoints = {
APP: string;
CMA: string;
DEVELOPER_HUB: string;
[key:string]:string;
};
22 changes: 15 additions & 7 deletions src/uiLocation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import postRobot from "post-robot";
import EventEmitter from "wolfy87-eventemitter";

Expand Down Expand Up @@ -26,15 +27,15 @@ import {
InitializationData,
LocationType,
Manifest,
Region,
IOrgFullPageLocation,
RegionType,
} from "./types";
import { GenericObjectType } from "./types/common.types";
import { User } from "./types/user.types";
import { formatAppRegion, onData, onError } from "./utils/utils";
import Window from "./window";
import { dispatchApiRequest, dispatchAdapter } from './utils/adapter';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ContentstackEndpoints } from "./types/api.type";

const emitter = new EventEmitter();

Expand Down Expand Up @@ -69,6 +70,9 @@ class UiLocation {
*/
private config: GenericObjectType;


readonly endpoints: ContentstackEndpoints

/**
* This holds the instance of Cross-domain communication library for posting messages between windows.
*/
Expand Down Expand Up @@ -103,7 +107,7 @@ class UiLocation {
/**
* The Contentstack Region on which the app is running.
*/
readonly region: Region;
readonly region: RegionType;
version: number | null;

ids: {
Expand Down Expand Up @@ -154,7 +158,7 @@ class UiLocation {
});

this.metadata = new Metadata(postRobot);

this.config = initializationData.config ?? {};

this.ids = {
Expand Down Expand Up @@ -184,7 +188,8 @@ class UiLocation {

this.modal = new Modal();

this.region = formatAppRegion(initializationData.region);
this.region = formatAppRegion(initializationData.region);
this.endpoints = initializationData.endpoints;

const stack = new Stack(initializationData.stack, postRobot, {
currentBranch: initializationData.currentBranch,
Expand Down Expand Up @@ -468,10 +473,13 @@ class UiLocation {
/**
* Method used to get the Contentstack Region on which the app is running.
*/
getCurrentRegion = (): Region => {
getCurrentRegion = (): RegionType => {
return this.region;
};

getCurrentEndpoints = ():ContentstackEndpoints => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel getEndpoints would be enough.

return this.endpoints;
}
/**
* Method used to make an API request to the Contentstack's CMA APIs.
*/
Expand All @@ -483,7 +491,7 @@ class UiLocation {
*/
createAdapter = (): (config: AxiosRequestConfig) => Promise<AxiosResponse> => {
return (config: AxiosRequestConfig): Promise<AxiosResponse> => {
return dispatchAdapter(postRobot)(config) as Promise<AxiosResponse>;
return dispatchAdapter(postRobot)(config)
};
};

Expand Down
67 changes: 36 additions & 31 deletions src/utils/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,52 @@
import PostRobot from 'post-robot';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { onError, fetchToAxiosConfig } from './utils';
import PostRobot from "post-robot";
import { AxiosRequestConfig, AxiosResponse, isAxiosError } from "axios";

import { onError, fetchToAxiosConfig, serializeAxiosResponse, handleApiError } from "./utils";

/**
* Dispatches a request using PostRobot.
* @param postRobot - The PostRobot instance.
* @returns A function that takes AxiosRequestConfig and returns a promise.
*/
export const dispatchAdapter = (postRobot: typeof PostRobot) => (config: AxiosRequestConfig)=> {
export const dispatchAdapter = (postRobot: typeof PostRobot) => (config: AxiosRequestConfig): Promise<AxiosResponse> => {
return postRobot
.sendToParent("apiAdapter", config )
.then(({ data }) => ({ ...data, config }))
.sendToParent("apiAdapter", config)
.then((event:unknown) => {
const { data } = event as { data: AxiosResponse };
if (data.status >= 400) {
throw serializeAxiosResponse(data, config);
}
return serializeAxiosResponse(data, config);
})
.catch(onError);
};

/**
* Dispatches an API request using axios and PostRobot.
* @param url - The URL of the API endpoint.
* @param options - Optional request options.
* @returns A promise that resolves to a partial Response object.
*/
export const dispatchApiRequest = async (url: string, options?: RequestInit): Promise<Response> => {
try {
const config = fetchToAxiosConfig(url, options);
const responseData = await dispatchAdapter(PostRobot)(config) as AxiosResponse;
return new Response(responseData.data,{
status: responseData.status,
statusText: responseData.statusText,
headers: new Headers(responseData.config.headers || {}),
});

} catch (error: any) {
if (error.response) {
const fetchResponse = new Response(error.response.data, {
status: error.response.status,
statusText: error.response.statusText,
headers: new Headers(error.response.headers)
});
return Promise.reject(fetchResponse);
} else if (error.request) {
return Promise.reject(new Response(null, { status: 0, statusText: 'Network Error' }));
} else {
return Promise.reject(new Response(null, { status: 0, statusText: error.message }));
}
export const dispatchApiRequest = async (
url: string,
options?: RequestInit,
): Promise<Response> => {
try {
const config = fetchToAxiosConfig(url, options);
const responseData = (await dispatchAdapter(PostRobot)(
config
)) as AxiosResponse;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this type assertion is needed now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes removing

if (isAxiosError(responseData)) {
throw responseData;
}
const response = new Response(responseData?.data, {
status: responseData.status,
statusText: responseData.statusText,
headers: new Headers(responseData.config.headers || {}),
});
return response

} catch (error) {
throw handleApiError(error);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to throw this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need errors to be catched on user side right? If I do return errors are going as response

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you are returning response object in all cases right? Can check your implementation is fetch compatible?

};
};
111 changes: 83 additions & 28 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Region } from "../types";
import { AxiosHeaders, AxiosRequestConfig } from "axios";
import {
AxiosError,
AxiosHeaders,
AxiosRequestConfig,
AxiosResponse,
isAxiosError,
} from "axios";

import { Region, RegionType } from "../types";

export function onData<Data extends Record<string, any>>(data: { data: Data }) {
if (typeof data.data === "string") {
Expand All @@ -12,11 +19,45 @@ export function onError(error: Error) {
return Promise.reject(error);
}

export function formatAppRegion(region: string): Region {
if (region && Object.values(Region).includes(region as Region)) {
return region as Region;
}
return Region.UNKNOWN;
export function axiosError(error: AxiosError) {
const { response, message } = error || {};
return {
data: response?.data || message,
status: response?.status || 500,
statusText: response?.statusText || "Internal Server Error",
headers: new Headers(
response?.headers ? Object.entries(response.headers) : undefined
),
};
}

export const handleApiError = (error: any): Response => {
return isAxiosError(error)
? createErrorResponse(axiosError(error))
: createErrorResponse(error);
};

export const createErrorResponse = (errorData: any): Response => {
const data = errorData.data || errorData.message || errorData;
const status = errorData.status || 500;
const statusText = errorData.statusText || 'Internal Server Error';
const headers = errorData.headers instanceof Headers
? errorData.headers
: new Headers(errorData.headers || {});

const responseBody = typeof data === 'string'
? data
: JSON.stringify(data);

return new Response(responseBody, {
status,
statusText,
headers
});
};

export function formatAppRegion(region: string): RegionType {
return region ?? Region.UNKNOWN;
}

export function getPreferredBodyElement(nodeCollection: HTMLCollection) {
Expand Down Expand Up @@ -50,36 +91,50 @@ export function getPreferredBodyElement(nodeCollection: HTMLCollection) {
return rootElement || nodeCollection[0];
}


export const convertHeaders = (headers: HeadersInit): AxiosHeaders => {
const axiosHeaders = new AxiosHeaders();
if (headers instanceof Headers) {
headers.forEach((value, key) => {
axiosHeaders.set(key, value);
});
headers.forEach((value, key) => {
axiosHeaders.set(key, value);
});
} else if (Array.isArray(headers)) {
headers.forEach(([key, value]) => {
axiosHeaders.set(key, value);
});
headers.forEach(([key, value]) => {
axiosHeaders.set(key, value);
});
} else {
Object.entries(headers).forEach(([key, value]) => {
axiosHeaders.set(key, value);
});
Object.entries(headers).forEach(([key, value]) => {
axiosHeaders.set(key, value);
});
}
return axiosHeaders;
};
};

export const fetchToAxiosConfig = (url: string, options: RequestInit = {}): AxiosRequestConfig => {
export const fetchToAxiosConfig = (
url: string,
options?: RequestInit
): AxiosRequestConfig => {
const axiosConfig: AxiosRequestConfig = {
url,
method: options.method || 'GET',
headers: options.headers ? convertHeaders({...options.headers}) : {},
data: options.body,
url,
method: options?.method || "GET",
headers: options?.headers
? convertHeaders({ ...options?.headers })
: {},
data: options?.body,
};
if (options.credentials === 'include') {
axiosConfig.withCredentials = true;

if (options?.credentials === "include") {
axiosConfig.withCredentials = true;
}

return axiosConfig;
}
};

export const serializeAxiosResponse = (responseData: AxiosResponse, config) => {
return {
data: responseData.data,
status: responseData.status,
statusText: responseData.statusText,
headers: responseData.headers as AxiosHeaders,
config,
};
};
Loading
Loading