diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24e4336..803ae7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,34 +15,6 @@ jobs: checks: write steps: - uses: actions/checkout@v3 - - name: Check working directory after checkout - run: pwd - - uses: actions/setup-node@v4 - with: - node-version: '22.x' - - name: Check working directory after setup-node - run: pwd - - name: Install dependencies - run: | - pwd - npm ci - - name: Download regions.json - run: | - pwd - ls -la - mkdir -p dist/lib - pwd - npm run download-regions - pwd - ls -la dist/lib/ || echo "dist/lib does not exist" - if [ ! -f dist/lib/regions.json ]; then - echo "Error: regions.json was not downloaded successfully" - exit 1 - fi - - name: Check working directory before tests - run: | - pwd - ls -la - uses: ArtiomTr/jest-coverage-report-action@v2 id: coverage-utils-js continue-on-error: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 27e3ac3..a951013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.6.1](https://github.com/contentstack/contentstack-utils-javascript/tree/v1.6.1) + - Fix: Improved error handling in getContentstackEndpoint + - Fix: Enhanced error messages for better debugging + - Refactor: Removed redundant try/catch wrapper and improved code structure + - Test: Updated and improved test coverage for error scenarios + ## [1.6.0](https://github.com/contentstack/contentstack-utils-javascript/tree/v1.6.0) - Feat: Adds Helper functions for Contentstack Endpoints diff --git a/__test__/endpoints.test.ts b/__test__/endpoints.test.ts index 92d8d86..8044e1b 100644 --- a/__test__/endpoints.test.ts +++ b/__test__/endpoints.test.ts @@ -2,12 +2,7 @@ import { getContentstackEndpoint, ContentstackEndpoints } from '../src/endpoints import * as path from 'path'; import * as fs from 'fs'; -// Mock console.warn to avoid noise in tests -const originalConsoleWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - // Verify build completed - dist/lib/regions.json must exist // The pretest hook ensures build runs before tests const regionsPath = path.join(process.cwd(), 'dist', 'lib', 'regions.json'); @@ -17,10 +12,6 @@ beforeAll(() => { } }); -afterAll(() => { - console.warn = originalConsoleWarn; -}); - describe('getContentstackEndpoint', () => { describe('Basic functionality', () => { it('should return default endpoints for valid region without service', () => { @@ -38,16 +29,16 @@ describe('getContentstackEndpoint', () => { expect(result).toBe('https://cdn.contentstack.io'); }); - it('should return EU endpoints for EU region', () => { - const result = getContentstackEndpoint('eu', 'contentDelivery'); - - expect(result).toBe('https://eu-cdn.contentstack.com'); + it('should throw error for invalid service', () => { + expect(() => { + getContentstackEndpoint('us', 'invalidService'); + }).toThrow(/Service "invalidService" not found for region/); }); - it('should return undefined for invalid service', () => { - const result = getContentstackEndpoint('us', 'invalidService'); - - expect(result).toBeUndefined(); + it('should throw error with exact error message format for invalid service', () => { + expect(() => { + getContentstackEndpoint('us', 'nonexistentService'); + }).toThrow('Service "nonexistentService" not found for region "na"'); }); }); @@ -103,40 +94,50 @@ describe('getContentstackEndpoint', () => { expect(result).toBe('https://cdn.contentstack.io'); }); - }); - describe('Error handling and edge cases', () => { - it('should throw error for empty region', () => { - expect(() => { - getContentstackEndpoint(''); - }).toThrow('Unable to set the host. Please put valid host'); + it('should strip https from EU endpoint when omitHttps is true', () => { + const result = getContentstackEndpoint('eu', 'contentDelivery', true); + + expect(result).toBe('eu-cdn.contentstack.com'); }); - it('should return default endpoint for invalid region', () => { - const result = getContentstackEndpoint('invalid-region', 'contentDelivery'); + it('should strip https from Azure endpoint when omitHttps is true', () => { + const result = getContentstackEndpoint('azure-na', 'contentDelivery', true); - expect(result).toBe('https://cdn.contentstack.io'); + expect(result).toBe('azure-na-cdn.contentstack.com'); }); - it('should return default endpoint for region with underscores/dashes', () => { - const result = getContentstackEndpoint('invalid_region_format', 'contentDelivery'); + it('should strip https from GCP endpoint when omitHttps is true', () => { + const result = getContentstackEndpoint('gcp-na', 'contentDelivery', true); - expect(result).toBe('https://cdn.contentstack.io'); + expect(result).toBe('gcp-na-cdn.contentstack.com'); }); - it('should handle malformed regions data gracefully', () => { - // Note: This test now verifies that invalid regions fallback to default endpoint - // The malformed data scenario is handled by getRegions() throwing an error - // which causes getContentstackEndpoint to fall back to getDefaultEndpoint - const result = getContentstackEndpoint('us', 'contentDelivery', false); + it('should strip https from all endpoints for EU region when omitHttps is true', () => { + const result = getContentstackEndpoint('eu', '', true) as ContentstackEndpoints; - expect(result).toBe('https://cdn.contentstack.io'); + expect(result.contentDelivery).toBe('eu-cdn.contentstack.com'); + expect(result.contentManagement).toBe('eu-api.contentstack.com'); }); + }); - it('should fallback to default when region is not found', () => { - const result = getContentstackEndpoint('nonexistent', 'contentDelivery'); - - expect(result).toBe('https://cdn.contentstack.io'); + describe('Error handling and edge cases', () => { + it('should throw error for empty region', () => { + expect(() => { + getContentstackEndpoint(''); + }).toThrow('Empty region provided. Please put valid region.'); + }); + + it('should throw error for invalid region', () => { + expect(() => { + getContentstackEndpoint('invalid-region', 'contentDelivery'); + }).toThrow('Invalid region: invalid-region'); + }); + + it('should throw error when region is not found', () => { + expect(() => { + getContentstackEndpoint('nonexistent', 'contentDelivery'); + }).toThrow('Invalid region: nonexistent'); }); }); @@ -154,6 +155,7 @@ describe('getContentstackEndpoint', () => { expect(result).toBeDefined(); expect(typeof result).toBe('object'); + expect((result as ContentstackEndpoints).contentDelivery).toBe('https://cdn.contentstack.io'); }); it('should use default omitHttps false when not provided', () => { @@ -161,6 +163,15 @@ describe('getContentstackEndpoint', () => { expect(result).toBe('https://cdn.contentstack.io'); }); + + it('should return all endpoints when service is empty string', () => { + const result = getContentstackEndpoint('us', ''); + + expect(result).toBeDefined(); + expect(typeof result).toBe('object'); + expect((result as ContentstackEndpoints).contentDelivery).toBe('https://cdn.contentstack.io'); + expect((result as ContentstackEndpoints).contentManagement).toBe('https://api.contentstack.io'); + }); }); describe('Service-specific endpoints', () => { @@ -230,8 +241,8 @@ describe('getContentstackEndpoint', () => { expect(result).toBe('https://ai.contentstack.com'); }); - it('should return correct personalize endpoint', () => { - const result = getContentstackEndpoint('us', 'personalize'); + it('should return correct personalizeManagement endpoint', () => { + const result = getContentstackEndpoint('us', 'personalizeManagement'); expect(result).toBe('https://personalize-api.contentstack.com'); }); @@ -243,27 +254,13 @@ describe('getContentstackEndpoint', () => { }); }); - describe('Different regions', () => { + describe('Additional regions and aliases', () => { it('should return correct EU endpoints', () => { const result = getContentstackEndpoint('eu', 'contentDelivery'); expect(result).toBe('https://eu-cdn.contentstack.com'); }); - it('should return correct Azure NA endpoints', () => { - const result = getContentstackEndpoint('azure-na', 'contentDelivery'); - - expect(result).toBe('https://azure-na-cdn.contentstack.com'); - }); - - it('should return correct GCP NA endpoints', () => { - const result = getContentstackEndpoint('gcp-na', 'contentDelivery'); - - expect(result).toBe('https://gcp-na-cdn.contentstack.com'); - }); - }); - - describe('Additional regions and aliases', () => { it('should return correct Australia endpoints', () => { const result = getContentstackEndpoint('au', 'contentDelivery'); @@ -276,12 +273,24 @@ describe('getContentstackEndpoint', () => { expect(result).toBe('https://au-cdn.contentstack.com'); }); + it('should return correct Azure NA endpoints', () => { + const result = getContentstackEndpoint('azure-na', 'contentDelivery'); + + expect(result).toBe('https://azure-na-cdn.contentstack.com'); + }); + it('should return correct Azure EU endpoints', () => { const result = getContentstackEndpoint('azure-eu', 'contentDelivery'); expect(result).toBe('https://azure-eu-cdn.contentstack.com'); }); + it('should return correct GCP NA endpoints', () => { + const result = getContentstackEndpoint('gcp-na', 'contentDelivery'); + + expect(result).toBe('https://gcp-na-cdn.contentstack.com'); + }); + it('should return correct GCP EU endpoints', () => { const result = getContentstackEndpoint('gcp-eu', 'contentDelivery'); @@ -302,53 +311,37 @@ describe('getContentstackEndpoint', () => { }); describe('Edge cases and error scenarios', () => { - it('should handle null region gracefully', () => { - const result = getContentstackEndpoint(null as any, 'contentDelivery'); - - expect(result).toBe('https://cdn.contentstack.io'); + it('should throw error for null region', () => { + expect(() => { + getContentstackEndpoint(null as any, 'contentDelivery'); + }).toThrow(); }); - it('should handle undefined region gracefully', () => { + it('should use default region for undefined region', () => { + // undefined uses the default parameter 'us', so it doesn't throw const result = getContentstackEndpoint(undefined as any, 'contentDelivery'); - expect(result).toBe('https://cdn.contentstack.io'); }); it('should handle region with only whitespace', () => { + // Whitespace gets trimmed, then normalized to 'us' if empty + // Since ' '.trim() is empty string, it normalizes to 'us' const result = getContentstackEndpoint(' ', 'contentDelivery'); - expect(result).toBe('https://cdn.contentstack.io'); }); - it('should handle region with special characters', () => { - const result = getContentstackEndpoint('region@#$%', 'contentDelivery'); - - expect(result).toBe('https://cdn.contentstack.io'); + it('should throw error for region with special characters', () => { + expect(() => { + getContentstackEndpoint('region@#$%', 'contentDelivery'); + }).toThrow('Invalid region: region@#$%'); }); - it('should handle very long region name', () => { + it('should throw error for very long region name', () => { const longRegion = 'a'.repeat(1000); - const result = getContentstackEndpoint(longRegion, 'contentDelivery'); - - expect(result).toBe('https://cdn.contentstack.io'); + expect(() => { + getContentstackEndpoint(longRegion, 'contentDelivery'); + }).toThrow(`Invalid region: ${longRegion}`); }); }); - describe('Console warnings', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should warn for invalid region', () => { - getContentstackEndpoint('invalid-region', 'contentDelivery'); - - expect(console.warn).toHaveBeenCalledWith('Invalid region combination.'); - }); - - it('should warn for failed endpoint fetch', () => { - getContentstackEndpoint('invalid-region', 'contentDelivery'); - - expect(console.warn).toHaveBeenCalledWith('Failed to fetch endpoints:', expect.any(Error)); - }); - }); }); diff --git a/eslint.config.js b/eslint.config.js index cd7fbf7..2a71b53 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,6 +16,7 @@ export default [ console: 'readonly', __dirname: 'readonly', require: 'readonly', + process: 'readonly', }, }, plugins: { diff --git a/package-lock.json b/package-lock.json index 7596cc7..c411eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/utils", - "version": "1.6.0", + "version": "1.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/utils", - "version": "1.6.0", + "version": "1.6.1", "hasInstallScript": true, "license": "MIT", "devDependencies": { @@ -88,6 +88,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", diff --git a/package.json b/package.json index f747970..892740c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/utils", - "version": "1.6.0", + "version": "1.6.1", "description": "Contentstack utilities for Javascript", "main": "dist/index.es.js", "types": "dist/types/src/index.d.ts", diff --git a/src/endpoints.ts b/src/endpoints.ts index ad2a280..e9f61e8 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -20,22 +20,40 @@ export interface RegionsResponse { regions: RegionData[]; } +// Declare __dirname for TypeScript (available at runtime in CommonJS output) +declare const __dirname: string; + // Load regions.json at runtime from the dist/lib directory function loadRegions(): RegionsResponse { - // Only look for regions.json in dist/lib directory - const regionsPath = path.join(process.cwd(), 'dist', 'lib', 'regions.json'); - - if (fs.existsSync(regionsPath)) { - try { - const regionsData = fs.readFileSync(regionsPath, 'utf-8'); - return JSON.parse(regionsData); - } catch (error) { - throw new Error(`Failed to parse regions.json: ${error instanceof Error ? error.message : String(error)}`); + // Get the directory of the current module + // In CommonJS (compiled output), __dirname is available at runtime + // When packed, the file structure may be different, so we check multiple paths + // __dirname will be available at runtime in CommonJS output from rollup + // Use __dirname directly since it's available at runtime in CommonJS output + const moduleDir = __dirname; + + // Try multiple possible paths: + // 1. lib/regions.json (relative to __dirname - for production/packed package) + // Main entry point is dist/index.es.js, so __dirname is dist, file is at dist/lib/regions.json + // 2. dist/lib/regions.json (relative to process.cwd() - for development/tests) + const possiblePaths = [ + path.join(moduleDir, 'lib', 'regions.json'), + path.join(process.cwd(), 'dist', 'lib', 'regions.json'), + ]; + + for (const regionsPath of possiblePaths) { + if (fs.existsSync(regionsPath)) { + try { + const regionsData = fs.readFileSync(regionsPath, 'utf-8'); + return JSON.parse(regionsData); + } catch (error) { + throw new Error(`Failed to parse regions.json: ${error instanceof Error ? error.message : String(error)}`); + } } } // If not found, throw clear error - throw new Error('regions.json file not found at dist/lib/regions.json. Please ensure the package is properly installed and postinstall script has run.'); + throw new Error('regions.json file not found. Please ensure the package is properly installed and postinstall script has run.'); } // Cache the loaded regions data @@ -51,71 +69,40 @@ function getRegions(): RegionsResponse { export function getContentstackEndpoint(region: string = 'us', service: string = '', omitHttps: boolean = false): string | ContentstackEndpoints { // Validate empty region before any processing if (region === '') { - console.warn('Invalid region: empty or invalid region provided'); - throw new Error('Unable to set the host. Please put valid host'); + throw new Error('Empty region provided. Please put valid region.'); } - try { - const regionsData: RegionsResponse = getRegions(); + const regionsData: RegionsResponse = getRegions(); - // Normalize the region input - const normalizedRegion = region.toLowerCase().trim() || 'us'; + // Normalize the region input + const normalizedRegion = region.toLowerCase().trim() || 'us'; - // Check if regions data is malformed - if (!Array.isArray(regionsData.regions)) { - throw new Error('Invalid Regions file. Please install the SDK again to fix this issue.'); - } + // Check if regions data is malformed + if (!Array.isArray(regionsData.regions)) { + throw new Error('Invalid Regions file. Please install the SDK again to fix this issue.'); + } - // Find the region by ID or alias - const regionData = findRegionByIDOrAlias(regionsData.regions, normalizedRegion); - - if (!regionData) { - // Check if this looks like a legacy format that should throw an error - if (region.includes('_') || region.includes('-')) { - const parts = region.split(/[-_]/); - if (parts.length >= 2) { - console.warn(`Invalid region combination.`); - throw new Error('Region Invalid. Please use a valid region identifier.'); - } - } - - console.warn('Invalid region:', region, '(normalized:', normalizedRegion + ')'); - console.warn('Failed to fetch endpoints:', new Error(`Invalid region: ${region}`)); - return getDefaultEndpoint(service, omitHttps); - } + // Find the region by ID or alias + const regionData = findRegionByIDOrAlias(regionsData.regions, normalizedRegion); - // Get the endpoint(s) - let endpoint: string | ContentstackEndpoints; + if (!regionData) { + throw new Error(`Invalid region: ${region}`); + } - if (service) { - // Return specific service endpoint - endpoint = regionData.endpoints[service]; + // Get the endpoint(s) + if (service) { + // Return specific service endpoint + const endpoint = regionData.endpoints[service]; - if (!endpoint) { - // For invalid services, return undefined (as expected by some tests) - return undefined as unknown as ContentstackEndpoints; - } - } else { - return omitHttps ? stripHttps(regionData.endpoints) : regionData.endpoints; + if (!endpoint) { + throw new Error(`Service "${service}" not found for region "${regionData.id}"`); } - return omitHttps ? stripHttps(endpoint) : endpoint; - } catch (error) { - console.warn('Failed to fetch endpoints:', error); - return getDefaultEndpoint(service, omitHttps); + } else { + return omitHttps ? stripHttps(regionData.endpoints) : regionData.endpoints; } } -function getDefaultEndpoint(service: string, omitHttps: boolean): string { - const regions = getRegions(); - const defaultEndpoints: ContentstackEndpoints = regions.regions.find((r: RegionData) => r.isDefault)?.endpoints || {}; - - const value = defaultEndpoints[service]; - const endpoint = typeof value === 'string' ? value : 'https://cdn.contentstack.io'; - - return omitHttps ? endpoint.replace(/^https?:\/\//, '') : endpoint; -} - function findRegionByIDOrAlias(regions: RegionData[], regionInput: string): RegionData | null { // First try to find by exact ID match let region = regions.find(r => r.id === regionInput);