From 095b3d7e1ae00c1f6d01431fce576573a02f6de9 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Tue, 19 Aug 2025 15:12:52 +0530 Subject: [PATCH 1/5] feat: add helper functions for endpoints --- .talismanrc | 4 +- CHANGELOG.md | 3 + __test__/endpoints.test.ts | 346 +++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/endpoints.ts | 97 +++++++++++ src/index.ts | 3 +- tsconfig.json | 1 + 7 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 __test__/endpoints.test.ts create mode 100644 src/endpoints.ts diff --git a/.talismanrc b/.talismanrc index 2d95750..24ea68c 100644 --- a/.talismanrc +++ b/.talismanrc @@ -7,4 +7,6 @@ fileignoreconfig: - filename: src/entry-editable.ts checksum: f9c4694229205fca252bb087482a3e408c6ad3b237cd108e337bcff49458db5c - filename: .husky/pre-commit - checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193 \ No newline at end of file + checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193 +- filename: src/endpoints.ts + checksum: 061295893d0ef7f3be959b65b857c543a4ad8439c07a1ecea2ebb5864eb99f18 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 000f8f4..2b441f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [1.5.0](https://github.com/contentstack/contentstack-utils-javascript/tree/v1.5.0) + - Feat: Adds Helper functions for Contentstack Endpoints + ## [1.4.1](https://github.com/contentstack/contentstack-utils-javascript/tree/v1.4.1) (2025-05-26) - Chore: Handle case sensitivity for contentType and locale diff --git a/__test__/endpoints.test.ts b/__test__/endpoints.test.ts new file mode 100644 index 0000000..e05ea62 --- /dev/null +++ b/__test__/endpoints.test.ts @@ -0,0 +1,346 @@ +import { getContentstackEndpoint, Region, ContentstackEndpoints } from '../src/endpoints'; + +// Mock fetch globally +Object.defineProperty(window, 'fetch', { + value: jest.fn(), + writable: true, +}); + +describe('getContentstackEndpoint', () => { + const mockFetch = fetch as jest.MockedFunction; + + beforeEach(() => { + mockFetch.mockClear(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const mockEndpointsData = { + "AWS": { + "NA": { + "CDA": "https://cdn.contentstack.io", + "CMA": "https://api.contentstack.io", + "Analytics": "https://app.contentstack.com", + "GraphQL": "https://graphql.contentstack.com", + "Personalize": { + "Management": "https://personalize-api.contentstack.com", + "Edge": "https://personalize-edge.contentstack.com" + } + }, + "EU": { + "CDA": "https://eu-cdn.contentstack.com", + "CMA": "https://eu-api.contentstack.com", + "Analytics": "https://eu-app.contentstack.com", + "GraphQL": "https://eu-graphql.contentstack.com" + }, + "AU": { + "CDA": "https://au-cdn.contentstack.com", + "CMA": "https://au-api.contentstack.com" + } + }, + "AZURE": { + "NA": { + "CDA": "https://azure-na-cdn.contentstack.com", + "CMA": "https://azure-na-api.contentstack.com" + }, + "EU": { + "CDA": "https://azure-eu-cdn.contentstack.com", + "CMA": "https://azure-eu-api.contentstack.com" + } + }, + "GCP": { + "NA": { + "CDA": "https://gcp-na-cdn.contentstack.com", + "CMA": "https://gcp-na-api.contentstack.com" + }, + "EU": { + "CDA": "https://gcp-eu-cdn.contentstack.com", + "CMA": "https://gcp-eu-api.contentstack.com" + } + } + }; + + describe('successful scenarios', () => { + beforeEach(() => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + redirected: false, + type: 'basic', + url: '', + clone: jest.fn(), + body: null, + bodyUsed: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + text: jest.fn(), + json: async () => mockEndpointsData, + } as unknown as Response); + }); + + test('should return US region endpoints by default', async () => { + const result = await getContentstackEndpoint(); + + expect(result).toEqual(mockEndpointsData.AWS.NA); + expect(mockFetch).toHaveBeenCalledWith('https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json'); + }); + + test('should return EU region endpoints', async () => { + const result = await getContentstackEndpoint(Region.EU); + + expect(result).toEqual(mockEndpointsData.AWS.EU); + }); + + test('should return AU region endpoints', async () => { + const result = await getContentstackEndpoint(Region.AU); + + expect(result).toEqual(mockEndpointsData.AWS.AU); + }); + + test('should return Azure NA region endpoints', async () => { + const result = await getContentstackEndpoint(Region.AZURE_NA); + + expect(result).toEqual(mockEndpointsData.AZURE.NA); + }); + + test('should return Azure EU region endpoints', async () => { + const result = await getContentstackEndpoint(Region.AZURE_EU); + + expect(result).toEqual(mockEndpointsData.AZURE.EU); + }); + + test('should return GCP NA region endpoints', async () => { + const result = await getContentstackEndpoint(Region.GCP_NA); + + expect(result).toEqual(mockEndpointsData.GCP.NA); + }); + + test('should return GCP EU region endpoints', async () => { + const result = await getContentstackEndpoint(Region.GCP_EU); + + expect(result).toEqual(mockEndpointsData.GCP.EU); + }); + + test('should return endpoints with HTTPS when omitHttps is false', async () => { + const result = await getContentstackEndpoint(Region.US, false); + + expect(result.CDA).toBe('https://cdn.contentstack.io'); + expect(result.CMA).toBe('https://api.contentstack.io'); + }); + + test('should return endpoints without HTTPS when omitHttps is true', async () => { + const result = await getContentstackEndpoint(Region.US, true); + + expect(result.CDA).toBe('cdn.contentstack.io'); + expect(result.CMA).toBe('api.contentstack.io'); + expect(result.Analytics).toBe('app.contentstack.com'); + }); + + test('should handle nested objects when omitHttps is true', async () => { + const result = await getContentstackEndpoint(Region.US, true); + + expect(result.Personalize).toEqual({ + "Management": "personalize-api.contentstack.com", + "Edge": "personalize-edge.contentstack.com" + }); + }); + + test('should preserve nested objects when omitHttps is false', async () => { + const result = await getContentstackEndpoint(Region.US, false); + + expect(result.Personalize).toEqual({ + "Management": "https://personalize-api.contentstack.com", + "Edge": "https://personalize-edge.contentstack.com" + }); + }); + }); + + describe('error scenarios', () => { + test('should throw error when fetch fails with network error', async () => { + mockFetch.mockRejectedValue(new Error('Network error')); + + await expect(getContentstackEndpoint()).rejects.toThrow('Network error'); + }); + + test('should throw error when HTTP response is not ok', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + headers: new Headers(), + redirected: false, + type: 'basic', + url: '', + clone: jest.fn(), + body: null, + bodyUsed: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + text: jest.fn(), + json: jest.fn(), + } as unknown as Response); + + await expect(getContentstackEndpoint()).rejects.toThrow( + 'Failed to fetch endpoints from https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json. HTTP status: 404 - Not Found' + ); + }); + + test('should throw error when JSON parsing fails', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + redirected: false, + type: 'basic', + url: '', + clone: jest.fn(), + body: null, + bodyUsed: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + text: jest.fn(), + json: async () => { + throw new Error('Invalid JSON'); + }, + } as unknown as Response); + + await expect(getContentstackEndpoint()).rejects.toThrow( + 'Failed to parse JSON response from https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json. Response may not be valid JSON.' + ); + }); + + test('should throw error for invalid region', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + redirected: false, + type: 'basic', + url: '', + clone: jest.fn(), + body: null, + bodyUsed: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + text: jest.fn(), + json: async () => mockEndpointsData, + } as unknown as Response); + + await expect(getContentstackEndpoint('invalid-region' as Region)).rejects.toThrow( + 'Invalid region: invalid-region. Supported regions are: us, eu, au, azure-na, azure-eu, gcp-na, gcp-eu' + ); + }); + + test('should throw error when region data is missing from JSON', async () => { + const incompleteData = { + "AWS": { + "NA": { + "CDA": "https://cdn.contentstack.io" + } + } + }; + + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + redirected: false, + type: 'basic', + url: '', + clone: jest.fn(), + body: null, + bodyUsed: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + text: jest.fn(), + json: async () => incompleteData, + } as unknown as Response); + + await expect(getContentstackEndpoint(Region.EU)).rejects.toThrow( + 'No endpoints found for region: eu (provider: AWS, region: EU)' + ); + }); + + test('should throw error when provider is missing from JSON', async () => { + const incompleteData = { + "AWS": { + "NA": { + "CDA": "https://cdn.contentstack.io" + } + } + }; + + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + redirected: false, + type: 'basic', + url: '', + clone: jest.fn(), + body: null, + bodyUsed: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + text: jest.fn(), + json: async () => incompleteData, + } as unknown as Response); + + await expect(getContentstackEndpoint(Region.AZURE_NA)).rejects.toThrow( + 'No endpoints found for region: azure-na (provider: AZURE, region: NA)' + ); + }); + }); + + describe('Region enum', () => { + test('should have correct region values', () => { + expect(Region.US).toBe('us'); + expect(Region.EU).toBe('eu'); + expect(Region.AU).toBe('au'); + expect(Region.AZURE_NA).toBe('azure-na'); + expect(Region.AZURE_EU).toBe('azure-eu'); + expect(Region.GCP_NA).toBe('gcp-na'); + expect(Region.GCP_EU).toBe('gcp-eu'); + }); + }); + + describe('ContentstackEndpoints interface', () => { + test('should accept string values', () => { + const endpoints: ContentstackEndpoints = { + CDA: 'https://cdn.contentstack.io', + CMA: 'https://api.contentstack.io' + }; + + expect(endpoints.CDA).toBe('https://cdn.contentstack.io'); + expect(endpoints.CMA).toBe('https://api.contentstack.io'); + }); + + test('should accept nested objects', () => { + const endpoints: ContentstackEndpoints = { + Personalize: { + Management: 'https://personalize-api.contentstack.com', + Edge: 'https://personalize-edge.contentstack.com' + } + }; + + expect(endpoints.Personalize).toEqual({ + Management: 'https://personalize-api.contentstack.com', + Edge: 'https://personalize-edge.contentstack.com' + }); + }); + }); +}); diff --git a/package.json b/package.json index c23bd12..5837b40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/utils", - "version": "1.4.1", + "version": "1.5.0", "description": "Contentstack utilities for Javascript", "main": "dist/index.es.js", "types": "dist/types/index.d.ts", diff --git a/src/endpoints.ts b/src/endpoints.ts new file mode 100644 index 0000000..b1b0d8c --- /dev/null +++ b/src/endpoints.ts @@ -0,0 +1,97 @@ +// Enum definition first +export enum Region { + US = "us", + EU = "eu", + AU = "au", + AZURE_NA = "azure-na", + AZURE_EU = "azure-eu", + GCP_NA = "gcp-na", + GCP_EU = "gcp-eu" +} + +// Type definitions +export interface ContentstackEndpoints { + [key: string]: string | ContentstackEndpoints; +} + +interface RegionEndpoints { + [provider: string]: { + [region: string]: ContentstackEndpoints; + }; +} + + + +// Default endpoint URL - should return the same structure as endpoints.json +const DEFAULT_ENDPOINTS_URL = 'https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json'; + +// Function to remove https prefix +function removeHttps(url: string): string { + return url.replace(/^https:\/\//, ''); +} + +// Map regions to the data structure paths +const regionToPath: { [key in Region]: string[] } = { + [Region.US]: ['AWS', 'NA'], + [Region.EU]: ['AWS', 'EU'], + [Region.AU]: ['AWS', 'AU'], + [Region.AZURE_NA]: ['AZURE', 'NA'], + [Region.AZURE_EU]: ['AZURE', 'EU'], + [Region.GCP_NA]: ['GCP', 'NA'], + [Region.GCP_EU]: ['GCP', 'EU'] +}; + +// Function to fetch endpoints from remote URL +async function fetchEndpointsData(url: string = DEFAULT_ENDPOINTS_URL): Promise { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch endpoints from ${url}. HTTP status: ${response.status} - ${response.statusText}`); + } + + try { + const endpointsData = await response.json(); + return endpointsData; + } catch (parseError) { + throw new Error(`Failed to parse JSON response from ${url}. Response may not be valid JSON.`); + } +} + +export async function getContentstackEndpoint(region: Region = Region.US, omitHttps: boolean = false): Promise { + // Fetch endpoints data from remote URL - will throw error if fails + const regionEndpoints = await fetchEndpointsData(); + + // Get the path for the specified region + const path = regionToPath[region]; + if (!path || path.length !== 2) { + throw new Error(`Invalid region: ${region}. Supported regions are: ${Object.values(Region).join(', ')}`); + } + + const [provider, regionKey] = path; + const endpoints: ContentstackEndpoints = regionEndpoints[provider]?.[regionKey]; + + if (!endpoints) { + throw new Error(`No endpoints found for region: ${region} (provider: ${provider}, region: ${regionKey})`); + } + + if (omitHttps) { + const result: ContentstackEndpoints = {}; + Object.entries(endpoints).forEach(([key, value]: [string, any]) => { + if (typeof value === 'string') { + result[key] = removeHttps(value); + } else if (typeof value === 'object' && value !== null) { + // Handle nested objects (like Personalize) + const nestedResult: { [key: string]: any } = {}; + Object.entries(value).forEach(([nestedKey, nestedValue]: [string, any]) => { + nestedResult[nestedKey] = typeof nestedValue === 'string' ? removeHttps(nestedValue) : nestedValue; + }); + result[key] = nestedResult; + } else { + result[key] = value; + } + }); + return result; + } + + return endpoints; +} diff --git a/src/index.ts b/src/index.ts index 37cf3f3..a2e7168 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,4 +13,5 @@ export { default as TextNode } from './nodes/text-node'; export { jsonToHTML } from './json-to-html' export { GQL } from './gql' export { addTags as addEditableTags } from './entry-editable' -export { updateAssetURLForGQL } from './updateAssetURLForGQL' \ No newline at end of file +export { updateAssetURLForGQL } from './updateAssetURLForGQL' +export { getContentstackEndpoint, Region, ContentstackEndpoints } from './endpoints' \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b4421ff..6b7a90f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ ], "types": ["jest"], "esModuleInterop": true, + "resolveJsonModule": true, "strictNullChecks": false, "sourceMap": true, }, From 2dd82f2da3c2358aa69589f4f6001722d4bcd24d Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Tue, 2 Sep 2025 12:25:26 +0530 Subject: [PATCH 2/5] refactor: simplify endpoint fetching logic and improve error handling --- __test__/endpoints.test.ts | 585 ++++++++++++++++++++----------------- src/endpoints.ts | 151 +++++----- 2 files changed, 376 insertions(+), 360 deletions(-) diff --git a/__test__/endpoints.test.ts b/__test__/endpoints.test.ts index e05ea62..a30f7d6 100644 --- a/__test__/endpoints.test.ts +++ b/__test__/endpoints.test.ts @@ -1,346 +1,379 @@ -import { getContentstackEndpoint, Region, ContentstackEndpoints } from '../src/endpoints'; +import { getContentstackEndpoint } from '../src/endpoints'; -// Mock fetch globally -Object.defineProperty(window, 'fetch', { - value: jest.fn(), - writable: true, -}); +// Mock the global fetch +const mockFetch = jest.fn(); +(globalThis as any).fetch = mockFetch; -describe('getContentstackEndpoint', () => { - const mockFetch = fetch as jest.MockedFunction; +// Mock console.warn and console.error to avoid noise in tests +const originalConsoleWarn = console.warn; +const originalConsoleError = console.error; - beforeEach(() => { - mockFetch.mockClear(); - }); +beforeEach(() => { + // Reset mocks before each test + mockFetch.mockClear(); + console.warn = jest.fn(); + console.error = jest.fn(); +}); - afterEach(() => { - jest.resetAllMocks(); - }); +afterAll(() => { + // Restore original console methods + console.warn = originalConsoleWarn; + console.error = originalConsoleError; +}); - const mockEndpointsData = { - "AWS": { - "NA": { - "CDA": "https://cdn.contentstack.io", - "CMA": "https://api.contentstack.io", - "Analytics": "https://app.contentstack.com", - "GraphQL": "https://graphql.contentstack.com", - "Personalize": { - "Management": "https://personalize-api.contentstack.com", - "Edge": "https://personalize-edge.contentstack.com" - } - }, - "EU": { - "CDA": "https://eu-cdn.contentstack.com", - "CMA": "https://eu-api.contentstack.com", - "Analytics": "https://eu-app.contentstack.com", - "GraphQL": "https://eu-graphql.contentstack.com" - }, - "AU": { - "CDA": "https://au-cdn.contentstack.com", - "CMA": "https://au-api.contentstack.com" - } +// Mock endpoints data structure +const mockEndpointsData = { + AWS: { + NA: { + CDA: 'https://cdn.contentstack.io', + CMA: 'https://api.contentstack.io', + GQL: 'https://graphql.contentstack.com' }, - "AZURE": { - "NA": { - "CDA": "https://azure-na-cdn.contentstack.com", - "CMA": "https://azure-na-api.contentstack.com" - }, - "EU": { - "CDA": "https://azure-eu-cdn.contentstack.com", - "CMA": "https://azure-eu-api.contentstack.com" - } + EU: { + CDA: 'https://eu-cdn.contentstack.com', + CMA: 'https://eu-api.contentstack.com', + GQL: 'https://eu-graphql.contentstack.com' }, - "GCP": { - "NA": { - "CDA": "https://gcp-na-cdn.contentstack.com", - "CMA": "https://gcp-na-api.contentstack.com" - }, - "EU": { - "CDA": "https://gcp-eu-cdn.contentstack.com", - "CMA": "https://gcp-eu-api.contentstack.com" - } + APAC: { + CDA: 'https://apac-cdn.contentstack.com', + CMA: 'https://apac-api.contentstack.com', + GQL: 'https://apac-graphql.contentstack.com' + } + }, + AZURE: { + NA: { + CDA: 'https://azure-na-cdn.contentstack.com', + CMA: 'https://azure-na-api.contentstack.com', + GQL: 'https://azure-na-graphql.contentstack.com' } - }; + } +}; - describe('successful scenarios', () => { +describe('getContentstackEndpoint', () => { + + describe('Successful fetch scenarios', () => { beforeEach(() => { mockFetch.mockResolvedValue({ ok: true, - status: 200, - statusText: 'OK', - headers: new Headers(), - redirected: false, - type: 'basic', - url: '', - clone: jest.fn(), - body: null, - bodyUsed: false, - arrayBuffer: jest.fn(), - blob: jest.fn(), - formData: jest.fn(), - text: jest.fn(), - json: async () => mockEndpointsData, - } as unknown as Response); - }); - - test('should return US region endpoints by default', async () => { + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + }); + + it('should return correct endpoint for default parameters (us region, CDA service)', async () => { const result = await getContentstackEndpoint(); - - expect(result).toEqual(mockEndpointsData.AWS.NA); - expect(mockFetch).toHaveBeenCalledWith('https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json'); + expect(result).toBe('https://cdn.contentstack.io'); }); - test('should return EU region endpoints', async () => { - const result = await getContentstackEndpoint(Region.EU); - - expect(result).toEqual(mockEndpointsData.AWS.EU); + it('should return endpoint without https when omitHttps is true', async () => { + const result = await getContentstackEndpoint('us', 'CDA', true); + expect(result).toBe('cdn.contentstack.io'); }); - test('should return AU region endpoints', async () => { - const result = await getContentstackEndpoint(Region.AU); - - expect(result).toEqual(mockEndpointsData.AWS.AU); + it('should handle "us" region and convert to aws_na', async () => { + const result = await getContentstackEndpoint('us', 'CDA'); + expect(result).toBe('https://cdn.contentstack.io'); }); - test('should return Azure NA region endpoints', async () => { - const result = await getContentstackEndpoint(Region.AZURE_NA); - - expect(result).toEqual(mockEndpointsData.AZURE.NA); + it('should handle "eu" region and convert to aws_eu', async () => { + const result = await getContentstackEndpoint('eu', 'CDA'); + expect(result).toBe('https://eu-cdn.contentstack.com'); }); - test('should return Azure EU region endpoints', async () => { - const result = await getContentstackEndpoint(Region.AZURE_EU); - - expect(result).toEqual(mockEndpointsData.AZURE.EU); + it('should handle "apac" region and convert to aws_apac', async () => { + const result = await getContentstackEndpoint('apac', 'CDA'); + expect(result).toBe('https://apac-cdn.contentstack.com'); }); - test('should return GCP NA region endpoints', async () => { - const result = await getContentstackEndpoint(Region.GCP_NA); - - expect(result).toEqual(mockEndpointsData.GCP.NA); + it('should handle region with underscore separator (aws_na)', async () => { + const result = await getContentstackEndpoint('aws_na', 'CDA'); + expect(result).toBe('https://cdn.contentstack.io'); }); - test('should return GCP EU region endpoints', async () => { - const result = await getContentstackEndpoint(Region.GCP_EU); - - expect(result).toEqual(mockEndpointsData.GCP.EU); + it('should handle region with hyphen separator (aws-na)', async () => { + const result = await getContentstackEndpoint('aws-na', 'CDA'); + expect(result).toBe('https://cdn.contentstack.io'); }); - test('should return endpoints with HTTPS when omitHttps is false', async () => { - const result = await getContentstackEndpoint(Region.US, false); - - expect(result.CDA).toBe('https://cdn.contentstack.io'); - expect(result.CMA).toBe('https://api.contentstack.io'); + it('should convert us region in aws_us format to aws_na', async () => { + const result = await getContentstackEndpoint('aws_us', 'CDA'); + expect(result).toBe('https://cdn.contentstack.io'); + }); + + it('should convert us region in aws-us format to aws_na', async () => { + const result = await getContentstackEndpoint('aws-us', 'CDA'); + expect(result).toBe('https://cdn.contentstack.io'); + }); + + it('should handle azure cloud provider', async () => { + const result = await getContentstackEndpoint('azure_na', 'CDA'); + expect(result).toBe('https://azure-na-cdn.contentstack.com'); + }); + + it('should handle azure cloud provider with hyphen', async () => { + const result = await getContentstackEndpoint('azure-na', 'CDA'); + expect(result).toBe('https://azure-na-cdn.contentstack.com'); + }); + + it('should handle different services (CMA)', async () => { + const result = await getContentstackEndpoint('us', 'CMA'); + expect(result).toBe('https://api.contentstack.io'); + }); + + it('should handle different services (GQL)', async () => { + const result = await getContentstackEndpoint('us', 'GQL'); + expect(result).toBe('https://graphql.contentstack.com'); + }); + + it('should handle case insensitive regions', async () => { + const result = await getContentstackEndpoint('EU', 'CDA'); + expect(result).toBe('https://eu-cdn.contentstack.com'); }); - test('should return endpoints without HTTPS when omitHttps is true', async () => { - const result = await getContentstackEndpoint(Region.US, true); + it('should handle mixed case regions with separators', async () => { + const result = await getContentstackEndpoint('AWS_EU', 'CDA'); + expect(result).toBe('https://eu-cdn.contentstack.com'); + }); + }); + + describe('Error scenarios', () => { + it('should return default host when fetch fails', async () => { + mockFetch.mockRejectedValue(new Error('Network error')); - expect(result.CDA).toBe('cdn.contentstack.io'); - expect(result.CMA).toBe('api.contentstack.io'); - expect(result.Analytics).toBe('app.contentstack.com'); + const result = await getContentstackEndpoint('us', 'CDA'); + expect(result).toBe('cdn.contentstack.io'); + expect(console.warn).toHaveBeenCalledWith('Failed to fetch endpoints:', expect.any(Error)); }); - test('should handle nested objects when omitHttps is true', async () => { - const result = await getContentstackEndpoint(Region.US, true); + it('should return default host when response is not ok', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404 + }); - expect(result.Personalize).toEqual({ - "Management": "personalize-api.contentstack.com", - "Edge": "personalize-edge.contentstack.com" + const result = await getContentstackEndpoint('us', 'CDA'); + expect(result).toBe('cdn.contentstack.io'); + }); + + it('should handle invalid JSON response and fall back to default', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue('invalid json') }); + + const result = await getContentstackEndpoint('us', 'CDA'); + expect(result).toBe('cdn.contentstack.io'); + expect(console.warn).toHaveBeenCalledWith('Failed to parse JSON response:', expect.any(Error)); + expect(console.warn).toHaveBeenCalledWith('Response content:', 'invalid json...'); + expect(console.warn).toHaveBeenCalledWith('Failed to fetch endpoints:', expect.any(Error)); }); - test('should preserve nested objects when omitHttps is false', async () => { - const result = await getContentstackEndpoint(Region.US, false); - - expect(result.Personalize).toEqual({ - "Management": "https://personalize-api.contentstack.com", - "Edge": "https://personalize-edge.contentstack.com" + it('should throw invalid JSON error when explicitly tested', async () => { + // Test that the JSON parsing actually does throw the error + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue('invalid json') }); + + // Create a special mock to test the error throwing behavior + const originalConsoleWarn = console.warn; + const consoleWarnSpy = jest.fn(); + console.warn = consoleWarnSpy; + + try { + // Manually trigger the JSON parsing by mocking a scenario + const response = await fetch('test'); + const result = await response.text(); + expect(() => JSON.parse(result)).toThrow(); + } catch { + // Expected to catch + } + + console.warn = originalConsoleWarn; }); - }); - describe('error scenarios', () => { - test('should throw error when fetch fails with network error', async () => { - mockFetch.mockRejectedValue(new Error('Network error')); + it('should handle empty region', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); - await expect(getContentstackEndpoint()).rejects.toThrow('Network error'); + await expect(getContentstackEndpoint('', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); + expect(console.warn).toHaveBeenCalledWith('Invalid region: empty or invalid region provided'); }); - test('should throw error when HTTP response is not ok', async () => { + it('should handle null region', async () => { mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: 'Not Found', - headers: new Headers(), - redirected: false, - type: 'basic', - url: '', - clone: jest.fn(), - body: null, - bodyUsed: false, - arrayBuffer: jest.fn(), - blob: jest.fn(), - formData: jest.fn(), - text: jest.fn(), - json: jest.fn(), - } as unknown as Response); - - await expect(getContentstackEndpoint()).rejects.toThrow( - 'Failed to fetch endpoints from https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json. HTTP status: 404 - Not Found' - ); - }); - - test('should throw error when JSON parsing fails', async () => { + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + const result = await getContentstackEndpoint(null as any, 'CDA'); + expect(result).toBe('cdn.contentstack.io'); + expect(console.warn).toHaveBeenCalledWith('Failed to fetch endpoints:', expect.any(Error)); + }); + + it('should handle undefined region', async () => { mockFetch.mockResolvedValue({ ok: true, - status: 200, - statusText: 'OK', - headers: new Headers(), - redirected: false, - type: 'basic', - url: '', - clone: jest.fn(), - body: null, - bodyUsed: false, - arrayBuffer: jest.fn(), - blob: jest.fn(), - formData: jest.fn(), - text: jest.fn(), - json: async () => { - throw new Error('Invalid JSON'); - }, - } as unknown as Response); - - await expect(getContentstackEndpoint()).rejects.toThrow( - 'Failed to parse JSON response from https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json. Response may not be valid JSON.' - ); - }); - - test('should throw error for invalid region', async () => { + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + const result = await getContentstackEndpoint(undefined, 'CDA'); + expect(result).toBe('https://cdn.contentstack.io'); + }); + + it('should handle invalid region format (single part)', async () => { mockFetch.mockResolvedValue({ ok: true, - status: 200, - statusText: 'OK', - headers: new Headers(), - redirected: false, - type: 'basic', - url: '', - clone: jest.fn(), - body: null, - bodyUsed: false, - arrayBuffer: jest.fn(), - blob: jest.fn(), - formData: jest.fn(), - text: jest.fn(), - json: async () => mockEndpointsData, - } as unknown as Response); - - await expect(getContentstackEndpoint('invalid-region' as Region)).rejects.toThrow( - 'Invalid region: invalid-region. Supported regions are: us, eu, au, azure-na, azure-eu, gcp-na, gcp-eu' - ); - }); - - test('should throw error when region data is missing from JSON', async () => { - const incompleteData = { - "AWS": { - "NA": { - "CDA": "https://cdn.contentstack.io" - } - } - }; + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + await expect(getContentstackEndpoint('invalid_region_format_with_too_many_parts', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); + expect(console.warn).toHaveBeenCalledWith('Invalid region format: invalid_region_format_with_too_many_parts'); + }); + it('should handle non-existent cloud provider', async () => { mockFetch.mockResolvedValue({ ok: true, - status: 200, - statusText: 'OK', - headers: new Headers(), - redirected: false, - type: 'basic', - url: '', - clone: jest.fn(), - body: null, - bodyUsed: false, - arrayBuffer: jest.fn(), - blob: jest.fn(), - formData: jest.fn(), - text: jest.fn(), - json: async () => incompleteData, - } as unknown as Response); - - await expect(getContentstackEndpoint(Region.EU)).rejects.toThrow( - 'No endpoints found for region: eu (provider: AWS, region: EU)' - ); - }); - - test('should throw error when provider is missing from JSON', async () => { - const incompleteData = { - "AWS": { - "NA": { - "CDA": "https://cdn.contentstack.io" - } - } + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + await expect(getContentstackEndpoint('invalid_na', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); + expect(console.warn).toHaveBeenCalledWith('Invalid region combination: INVALID_NA - CDA'); + }); + + it('should handle non-existent region for valid cloud provider', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + await expect(getContentstackEndpoint('aws_invalid', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); + expect(console.warn).toHaveBeenCalledWith('Invalid region combination: AWS_INVALID - CDA'); + }); + + it('should handle non-existent service for valid cloud and region', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + const result = await getContentstackEndpoint('aws_na', 'INVALID_SERVICE'); + expect(result).toBe(undefined); + }); + + it('should handle malformed endpoints data structure', async () => { + const malformedData = { + AWS: 'invalid structure' }; + + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(malformedData)) + }); + + await expect(getContentstackEndpoint('aws_na', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); + expect(console.warn).toHaveBeenCalledWith('Invalid region combination: AWS_NA - CDA'); + }); + it('should re-throw host validation errors from catch block', async () => { mockFetch.mockResolvedValue({ ok: true, - status: 200, - statusText: 'OK', - headers: new Headers(), - redirected: false, - type: 'basic', - url: '', - clone: jest.fn(), - body: null, - bodyUsed: false, - arrayBuffer: jest.fn(), - blob: jest.fn(), - formData: jest.fn(), - text: jest.fn(), - json: async () => incompleteData, - } as unknown as Response); - - await expect(getContentstackEndpoint(Region.AZURE_NA)).rejects.toThrow( - 'No endpoints found for region: azure-na (provider: AZURE, region: NA)' - ); + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + // This will trigger the catch block but the error should be re-thrown + await expect(getContentstackEndpoint('invalid_format', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); }); }); - describe('Region enum', () => { - test('should have correct region values', () => { - expect(Region.US).toBe('us'); - expect(Region.EU).toBe('eu'); - expect(Region.AU).toBe('au'); - expect(Region.AZURE_NA).toBe('azure-na'); - expect(Region.AZURE_EU).toBe('azure-eu'); - expect(Region.GCP_NA).toBe('gcp-na'); - expect(Region.GCP_EU).toBe('gcp-eu'); + describe('Edge cases and special scenarios', () => { + it('should handle text response parsing error in text() method', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockRejectedValue(new Error('Text parsing error')) + }); + + const result = await getContentstackEndpoint('us', 'CDA'); + expect(result).toBe('cdn.contentstack.io'); + expect(console.warn).toHaveBeenCalledWith('Failed to fetch endpoints:', expect.any(Error)); }); - }); - describe('ContentstackEndpoints interface', () => { - test('should accept string values', () => { - const endpoints: ContentstackEndpoints = { - CDA: 'https://cdn.contentstack.io', - CMA: 'https://api.contentstack.io' + it('should handle endpoints data with null values', async () => { + const dataWithNulls = { + AWS: { + NA: { + CDA: null as any + } + } }; - expect(endpoints.CDA).toBe('https://cdn.contentstack.io'); - expect(endpoints.CMA).toBe('https://api.contentstack.io'); + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(dataWithNulls)) + }); + + const result = await getContentstackEndpoint('aws_na', 'CDA'); + expect(result).toBe(null); + }); + + it('should handle endpoints with different protocol (http)', async () => { + const dataWithHttp = { + AWS: { + NA: { + CDA: 'http://cdn.contentstack.io' + } + } + }; + + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(dataWithHttp)) + }); + + const result = await getContentstackEndpoint('aws_na', 'CDA', true); + expect(result).toBe('cdn.contentstack.io'); }); - test('should accept nested objects', () => { - const endpoints: ContentstackEndpoints = { - Personalize: { - Management: 'https://personalize-api.contentstack.com', - Edge: 'https://personalize-edge.contentstack.com' + it('should handle endpoints without protocol when omitHttps is true', async () => { + const dataWithoutProtocol = { + AWS: { + NA: { + CDA: 'cdn.contentstack.io' + } } }; - expect(endpoints.Personalize).toEqual({ - Management: 'https://personalize-api.contentstack.com', - Edge: 'https://personalize-edge.contentstack.com' + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(dataWithoutProtocol)) }); + + const result = await getContentstackEndpoint('aws_na', 'CDA', true); + expect(result).toBe('cdn.contentstack.io'); + }); + + it('should handle complex region names with multiple separators', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(JSON.stringify(mockEndpointsData)) + }); + + // This should be processed but result in invalid format + await expect(getContentstackEndpoint('aws_na_extra', 'CDA')).rejects.toThrow('Unable to set the host. Please put valid host'); + }); + + it('should handle very long response content in error message', async () => { + const longInvalidJson = 'x'.repeat(500); + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(longInvalidJson) + }); + + const result = await getContentstackEndpoint('us', 'CDA'); + expect(result).toBe('cdn.contentstack.io'); + expect(console.warn).toHaveBeenCalledWith('Response content:', longInvalidJson.substring(0, 200) + '...'); + expect(console.warn).toHaveBeenCalledWith('Failed to fetch endpoints:', expect.any(Error)); }); }); }); diff --git a/src/endpoints.ts b/src/endpoints.ts index b1b0d8c..56dd467 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -1,97 +1,80 @@ -// Enum definition first -export enum Region { - US = "us", - EU = "eu", - AU = "au", - AZURE_NA = "azure-na", - AZURE_EU = "azure-eu", - GCP_NA = "gcp-na", - GCP_EU = "gcp-eu" -} - -// Type definitions export interface ContentstackEndpoints { [key: string]: string | ContentstackEndpoints; } -interface RegionEndpoints { - [provider: string]: { - [region: string]: ContentstackEndpoints; - }; -} - - - // Default endpoint URL - should return the same structure as endpoints.json -const DEFAULT_ENDPOINTS_URL = 'https://raw.githubusercontent.com/contentstack/contentstack-endpoints/master/src/endpoints.json'; - -// Function to remove https prefix -function removeHttps(url: string): string { - return url.replace(/^https:\/\//, ''); -} +const DEFAULT_ENDPOINTS_URL = 'https://raw.githubusercontent.com/nadeem-cs/cs-endpoints/refs/heads/main/endpoints.json'; -// Map regions to the data structure paths -const regionToPath: { [key in Region]: string[] } = { - [Region.US]: ['AWS', 'NA'], - [Region.EU]: ['AWS', 'EU'], - [Region.AU]: ['AWS', 'AU'], - [Region.AZURE_NA]: ['AZURE', 'NA'], - [Region.AZURE_EU]: ['AZURE', 'EU'], - [Region.GCP_NA]: ['GCP', 'NA'], - [Region.GCP_EU]: ['GCP', 'EU'] -}; -// Function to fetch endpoints from remote URL -async function fetchEndpointsData(url: string = DEFAULT_ENDPOINTS_URL): Promise { - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Failed to fetch endpoints from ${url}. HTTP status: ${response.status} - ${response.statusText}`); - } - +export async function getContentstackEndpoint(region: string = 'us', service: string = 'CDA', omitHttps: boolean = false): Promise { try { - const endpointsData = await response.json(); - return endpointsData; - } catch (parseError) { - throw new Error(`Failed to parse JSON response from ${url}. Response may not be valid JSON.`); - } -} - -export async function getContentstackEndpoint(region: Region = Region.US, omitHttps: boolean = false): Promise { - // Fetch endpoints data from remote URL - will throw error if fails - const regionEndpoints = await fetchEndpointsData(); - - // Get the path for the specified region - const path = regionToPath[region]; - if (!path || path.length !== 2) { - throw new Error(`Invalid region: ${region}. Supported regions are: ${Object.values(Region).join(', ')}`); - } - - const [provider, regionKey] = path; - const endpoints: ContentstackEndpoints = regionEndpoints[provider]?.[regionKey]; - - if (!endpoints) { - throw new Error(`No endpoints found for region: ${region} (provider: ${provider}, region: ${regionKey})`); - } + const response = await fetch(DEFAULT_ENDPOINTS_URL); + + if (response.ok) { + const result = await response.text(); + let endpointsData; + + try { + endpointsData = JSON.parse(result); + } catch (jsonError) { + console.warn('Failed to parse JSON response:', jsonError); + console.warn('Response content:', result.substring(0, 200) + '...'); + throw new Error('Invalid JSON response from endpoints service'); + } + + // Normalize region name + let normalizedRegion = region.toLowerCase(); + + // Convert 'us' to 'aws_na' and handle existing cloud_us patterns + if (normalizedRegion === 'us') { + normalizedRegion = 'aws_na'; + } else if (normalizedRegion.includes('_') || normalizedRegion.includes('-')) { + // Handle case where cloud provider is already included (e.g., 'aws_us' -> 'aws_na' or 'aws-us' -> 'aws_na') + const separator = normalizedRegion.includes('_') ? '_' : '-'; + const parts = normalizedRegion.split(separator); + if (parts.length === 2 && parts[1] === 'us') { + normalizedRegion = `${parts[0]}_na`; + } else if (parts.length === 2) { + // Convert hyphen to underscore for consistency + normalizedRegion = `${parts[0]}_${parts[1]}`; + } + } else if (!normalizedRegion.includes('_') && !normalizedRegion.includes('-') && normalizedRegion) { + // If region doesn't contain a cloud provider separator, append 'aws' + normalizedRegion = `aws_${normalizedRegion}`; + } + + if (normalizedRegion) { + const parts = normalizedRegion.toUpperCase().split('_'); + if (parts.length === 2) { + const [cloud, region] = parts; + + try { + const endpoint = endpointsData[cloud][region][service]; - if (omitHttps) { - const result: ContentstackEndpoints = {}; - Object.entries(endpoints).forEach(([key, value]: [string, any]) => { - if (typeof value === 'string') { - result[key] = removeHttps(value); - } else if (typeof value === 'object' && value !== null) { - // Handle nested objects (like Personalize) - const nestedResult: { [key: string]: any } = {}; - Object.entries(value).forEach(([nestedKey, nestedValue]: [string, any]) => { - nestedResult[nestedKey] = typeof nestedValue === 'string' ? removeHttps(nestedValue) : nestedValue; - }); - result[key] = nestedResult; + return omitHttps ? endpoint.replace(/^https?:\/\//, '') : endpoint; + } catch (error) { + console.warn(`Invalid region combination: ${cloud}_${region} - ${service}`); + throw Error('Unable to set the host. Please put valid host'); + } + } else { + // Handle invalid region format (not cloud_region pattern) + console.warn(`Invalid region format: ${normalizedRegion}`); + throw Error('Unable to set the host. Please put valid host'); + } } else { - result[key] = value; + // Handle empty or falsy region + console.warn('Invalid region: empty or invalid region provided'); + throw Error('Unable to set the host. Please put valid host'); } - }); - return result; + } + } catch (error) { + // Re-throw errors that are explicitly thrown by our logic + if (error instanceof Error && error.message === 'Unable to set the host. Please put valid host') { + throw error; + } + // If fetch fails or any other error occurs, return default host + console.warn('Failed to fetch endpoints:', error); } - - return endpoints; + + return 'cdn.contentstack.io'; } From 892855c6ac6c49df7d32e63dbf8bd582373954fc Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Tue, 2 Sep 2025 12:28:17 +0530 Subject: [PATCH 3/5] fix: remove unused export of region from endpoints --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index a2e7168..8d0d7c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,4 +14,4 @@ export { jsonToHTML } from './json-to-html' export { GQL } from './gql' export { addTags as addEditableTags } from './entry-editable' export { updateAssetURLForGQL } from './updateAssetURLForGQL' -export { getContentstackEndpoint, Region, ContentstackEndpoints } from './endpoints' \ No newline at end of file +export { getContentstackEndpoint, ContentstackEndpoints } from './endpoints' \ No newline at end of file From a7a8caa0a763323ba0acb8910bea03c1ea4c2606 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Sat, 6 Sep 2025 15:10:04 +0530 Subject: [PATCH 4/5] refactor: update getContentstackEndpoint function to improve region normalization and error handling --- .commitlintrc.json | 6 +---- .talismanrc | 2 +- src/endpoints.ts | 64 ++++++++++++++++++++++------------------------ 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/.commitlintrc.json b/.commitlintrc.json index 0b1a411..b40de1a 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,11 +1,7 @@ { "extends": ["@commitlint/config-conventional"], "rules": { - "subject-case": [ - 2, - "always", - ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] - ], + "subject-case": [0], "subject-empty": [2, "never"], "subject-full-stop": [2, "never", "."], "type-enum": [ diff --git a/.talismanrc b/.talismanrc index 24ea68c..5929f46 100644 --- a/.talismanrc +++ b/.talismanrc @@ -9,4 +9,4 @@ fileignoreconfig: - filename: .husky/pre-commit checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193 - filename: src/endpoints.ts - checksum: 061295893d0ef7f3be959b65b857c543a4ad8439c07a1ecea2ebb5864eb99f18 \ No newline at end of file + checksum: 721a1df93b02d04c1c19a76c171fe2748e4abb1fc3e43452e76fecfd8f384751 \ No newline at end of file diff --git a/src/endpoints.ts b/src/endpoints.ts index 56dd467..833d286 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -6,7 +6,7 @@ export interface ContentstackEndpoints { const DEFAULT_ENDPOINTS_URL = 'https://raw.githubusercontent.com/nadeem-cs/cs-endpoints/refs/heads/main/endpoints.json'; -export async function getContentstackEndpoint(region: string = 'us', service: string = 'CDA', omitHttps: boolean = false): Promise { +export async function getContentstackEndpoint(region: string = 'us', service: string = '', omitHttps: boolean = false): Promise { try { const response = await fetch(DEFAULT_ENDPOINTS_URL); @@ -22,59 +22,57 @@ export async function getContentstackEndpoint(region: string = 'us', service: st throw new Error('Invalid JSON response from endpoints service'); } - // Normalize region name - let normalizedRegion = region.toLowerCase(); - - // Convert 'us' to 'aws_na' and handle existing cloud_us patterns - if (normalizedRegion === 'us') { - normalizedRegion = 'aws_na'; - } else if (normalizedRegion.includes('_') || normalizedRegion.includes('-')) { - // Handle case where cloud provider is already included (e.g., 'aws_us' -> 'aws_na' or 'aws-us' -> 'aws_na') + let normalizedRegion = region.toUpperCase(); + + // Convert 'US' to 'aws_na' and handle existing patterns + if (normalizedRegion === 'US') { + normalizedRegion = 'AWS-NA'; + } else if (normalizedRegion.includes('_') || normalizedRegion.includes('-')) { // (e.g., 'aws_us' -> 'aws_na' or 'aws-us' -> 'aws-na') const separator = normalizedRegion.includes('_') ? '_' : '-'; const parts = normalizedRegion.split(separator); - if (parts.length === 2 && parts[1] === 'us') { - normalizedRegion = `${parts[0]}_na`; + if (parts.length === 2 && parts[1] === 'US') { + normalizedRegion = `${parts[0]}-NA`; } else if (parts.length === 2) { - // Convert hyphen to underscore for consistency - normalizedRegion = `${parts[0]}_${parts[1]}`; + normalizedRegion = `${parts[0]}-${parts[1]}`; } } else if (!normalizedRegion.includes('_') && !normalizedRegion.includes('-') && normalizedRegion) { - // If region doesn't contain a cloud provider separator, append 'aws' - normalizedRegion = `aws_${normalizedRegion}`; + normalizedRegion = `AWS-${normalizedRegion}`; } if (normalizedRegion) { - const parts = normalizedRegion.toUpperCase().split('_'); + const parts = normalizedRegion.toUpperCase().split('-'); if (parts.length === 2) { const [cloud, region] = parts; try { - const endpoint = endpointsData[cloud][region][service]; + const endpoint = service ? endpointsData[cloud][region][service] : endpointsData[cloud][region]; + endpoint['Region'] = normalizedRegion; - return omitHttps ? endpoint.replace(/^https?:\/\//, '') : endpoint; + return omitHttps ? stripHttps(endpoint) : endpoint; } catch (error) { - console.warn(`Invalid region combination: ${cloud}_${region} - ${service}`); - throw Error('Unable to set the host. Please put valid host'); + throw Error(`Invalid region combination: ${cloud}-${region} - ${service || 'all'}`); } } else { - // Handle invalid region format (not cloud_region pattern) - console.warn(`Invalid region format: ${normalizedRegion}`); - throw Error('Unable to set the host. Please put valid host'); + throw Error(`Invalid region format: ${normalizedRegion}`); } } else { // Handle empty or falsy region - console.warn('Invalid region: empty or invalid region provided'); - throw Error('Unable to set the host. Please put valid host'); + throw Error('Invalid region: empty or invalid region provided'); } } } catch (error) { - // Re-throw errors that are explicitly thrown by our logic - if (error instanceof Error && error.message === 'Unable to set the host. Please put valid host') { - throw error; - } - // If fetch fails or any other error occurs, return default host - console.warn('Failed to fetch endpoints:', error); + throw error; } - - return 'cdn.contentstack.io'; } + +function stripHttps(endpoint: string | ContentstackEndpoints): string | ContentstackEndpoints { + if (typeof endpoint === 'string') { + return endpoint.replace(/^https?:\/\//, ''); + } else { + const result: ContentstackEndpoints = {}; + for (const key in endpoint) { + result[key] = stripHttps(endpoint[key]); + } + return result; + } +} \ No newline at end of file From 24244a9a1c0050d7fcea7a2ef180ab52407dec14 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Mon, 22 Sep 2025 10:28:27 +0530 Subject: [PATCH 5/5] fix: add Region property conditionally to endpoint object in getContentstackEndpoint --- src/endpoints.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/endpoints.ts b/src/endpoints.ts index 833d286..f83b903 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -46,7 +46,11 @@ export async function getContentstackEndpoint(region: string = 'us', service: st try { const endpoint = service ? endpointsData[cloud][region][service] : endpointsData[cloud][region]; - endpoint['Region'] = normalizedRegion; + + // Only add Region property if endpoint is an object (not a string) + if (typeof endpoint === 'object' && endpoint !== null) { + endpoint['Region'] = normalizedRegion; + } return omitHttps ? stripHttps(endpoint) : endpoint; } catch (error) {