Skip to content
Closed
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
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 as AxiosResponse;
})
.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.
*/
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) {
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,
});
}
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