Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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;
};

getEndpoints = ():ContentstackEndpoints => {
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
84 changes: 50 additions & 34 deletions src/utils/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,63 @@
import PostRobot from 'post-robot';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { onError, fetchToAxiosConfig } from './utils';
import PostRobot from "post-robot";
import { Response } from "node-fetch";
import {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
} from "axios";

import {
onError,
fetchToAxiosConfig,
handleApiError,
sanitizeResponseHeader,
} 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)=> {
return postRobot
.sendToParent("apiAdapter", config )
.then(({ data }) => ({ ...data, config }))
.catch(onError);
};

export const dispatchAdapter =
(postRobot: typeof PostRobot) =>
(config: AxiosRequestConfig): Promise<AxiosResponse> => {
return postRobot
.sendToParent("apiAdapter", config)
.then((event: unknown) => {
const { data } = event as { data: AxiosResponse };
if (data.status >= 400) {
throw data
}
return data;
})
.catch((err)=>onError(err));
};
/**
* 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.
*/
Comment thread
Amitkanswal marked this conversation as resolved.
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;


return new Response(responseData?.data, {
status: responseData.status,
statusText: responseData.statusText,
url: responseData.config.url,
headers: new Headers(
sanitizeResponseHeader(responseData.config.headers || {})
),
});
} catch (error) {
return handleApiError(error as AxiosResponse | AxiosError);
}
}
};
};
104 changes: 77 additions & 27 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import { Region } from "../types";
import { AxiosHeaders, AxiosRequestConfig } from "axios";
import {
AxiosError,
AxiosHeaders,
AxiosRequestConfig,
AxiosResponse,
isAxiosError,
RawAxiosRequestHeaders,
} from "axios";
import { Response } from "node-fetch";

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

const filterHeaders = [
"api_key",
"authorization",
"auth_token",
"x-api-key",
"user-agent",
];

export function onData<Data extends Record<string, any>>(data: { data: Data }) {
if (typeof data.data === "string") {
Expand All @@ -12,11 +29,40 @@ 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;
export function sanitizeResponseHeader(axiosHeaders) {
Comment thread
Amitkanswal marked this conversation as resolved.
Outdated
const fetchHeaders = new Headers();
for (const key in axiosHeaders) {
if (axiosHeaders.hasOwnProperty(key) && !filterHeaders.includes(key)) {
fetchHeaders.append(key, axiosHeaders[key]);
}
}
return Region.UNKNOWN;
return fetchHeaders;
};
export const handleApiError = (error: AxiosResponse | AxiosError): Response => {
// Extract relevant information from the error
const isServerError = (error as AxiosResponse).status >= 500;
const responseBody = isServerError
? (error as AxiosError).stack || "Internal Server Error"
: (error as AxiosResponse).data || "An error occurred";

const status = (error as AxiosResponse).status || 500;
const statusText =
isServerError
? (error as AxiosError).message || "Internal Server Error"
: (error as AxiosResponse).statusText || "Error";

const headers = new Headers(
sanitizeResponseHeader((error as AxiosResponse).headers || {})
);

return new Response(JSON.stringify(responseBody), {
status,
statusText,
headers,
});
Comment thread
Amitkanswal marked this conversation as resolved.
Outdated
}
export function formatAppRegion(region: string): RegionType {
return region ?? Region.UNKNOWN;
}

export function getPreferredBodyElement(nodeCollection: HTMLCollection) {
Expand Down Expand Up @@ -50,36 +96,40 @@ 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;
}
};
Loading
Loading