diff --git a/.gitignore b/.gitignore index 0041e52..4887fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ coverage dist/* *.log .nx/ +regions.json \ No newline at end of file diff --git a/.npmignore b/.npmignore index 47e14fe..fce3424 100644 --- a/.npmignore +++ b/.npmignore @@ -19,4 +19,5 @@ src *.tgz .talismanrc tap-html.html -.github \ No newline at end of file +.github +regions.json \ No newline at end of file diff --git a/.talismanrc b/.talismanrc index 2aa5661..ea390f5 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,6 +1,10 @@ fileignoreconfig: - filename: package-lock.json - checksum: 32308dbe614c142c4804ff7c81baedddba058c5458e1d233fefb1d8070bf1905 + checksum: 275bc45fd72f2a19f8634536e1e0ea3d6516ea554178d172f9e64d01521b06f7 +- filename: test/unit/contentstack.spec.ts + checksum: d5b99c01459ab8bc597baaa9e6cc4aa91ac6d9bf78af08e1d0220d0c5db3d0b3 +- filename: test/unit/utils.spec.ts + checksum: 79ce5bd78376db37a34df82c0fea19031e995b66a5a246e73f8262fa05d65a9c - filename: test/unit/query-optimization-comprehensive.spec.ts checksum: f5aaf6c784d7c101a05ca513c584bbd6e95f963d1e42779f2596050d9bcbac96 - filename: src/lib/entries.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c38140..5652003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### Version: 4.10.2 +#### Date: Oct-29-2025 +Enhancement: Added logHandler interceptors for request and response logging + ### Version: 4.10.1 #### Date: Oct-27-2025 Fix: Upgrade dependecies diff --git a/package-lock.json b/package-lock.json index ed6a740..78b7109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@contentstack/delivery-sdk", - "version": "4.10.1", + "version": "4.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/delivery-sdk", - "version": "4.10.1", + "version": "4.10.2", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@contentstack/core": "^1.3.1", "@contentstack/utils": "^1.5.0", - "axios": "^1.12.2", + "axios": "^1.13.1", "humps": "^2.0.1" }, "devDependencies": { @@ -3992,17 +3993,17 @@ } }, "node_modules/@slack/bolt": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.5.0.tgz", - "integrity": "sha512-1YbgO/UDLYa0vOtGsTohpnl/dSKwo7RbUd29IJMfqNDLn+t81MmIL0w2KPNjZJQLsoevTRNCdHDeh4PJyY8DIA==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.6.0.tgz", + "integrity": "sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ==", "dev": true, "license": "MIT", "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^3.0.4", "@slack/socket-mode": "^2.0.5", - "@slack/types": "^2.17.0", - "@slack/web-api": "^7.11.0", + "@slack/types": "^2.18.0", + "@slack/web-api": "^7.12.0", "axios": "^1.12.0", "express": "^5.0.0", "path-to-regexp": "^8.1.0", @@ -4242,16 +4243,16 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.4.tgz", - "integrity": "sha512-g64dbryHk7loCIrsa0R3shBnEu5p6LPJ09bu9NG58+jz+cRUjFrc3Bz0kNQ7j9bXeCsrRDvNET1G54P/GJkAyA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { @@ -4378,9 +4379,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", "dependencies": { @@ -5035,9 +5036,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -6199,9 +6200,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.240", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", - "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", + "version": "1.5.243", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.243.tgz", + "integrity": "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index f7202f7..893eba3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/delivery-sdk", - "version": "4.10.1", + "version": "4.10.2", "type": "module", "license": "MIT", "main": "./dist/legacy/index.cjs", @@ -32,18 +32,21 @@ "build:cjs": "node tools/cleanup cjs && tsc -p config/tsconfig.cjs.json && node tools/rename-cjs.cjs", "build:esm": "node tools/cleanup esm && tsc -p config/tsconfig.esm.json", "build:types": "node tools/cleanup types && tsc -p config/tsconfig.types.json", - "husky-check": "npm run build && husky && chmod +x .husky/pre-commit" + "husky-check": "npm run build && husky && chmod +x .husky/pre-commit", + "postinstall": "curl -s --max-time 30 --fail https://artifacts.contentstack.com/regions.json -o src/assets/regions.json || echo 'Warning: Failed to download regions.json, using existing file if available'", + "postupdate": "curl -s --max-time 30 --fail https://artifacts.contentstack.com/regions.json -o src/assets/regions.json || echo 'Warning: Failed to download regions.json, using existing file if available'" }, "dependencies": { "@contentstack/core": "^1.3.1", "@contentstack/utils": "^1.5.0", - "axios": "^1.12.2", + "axios": "^1.13.1", "humps": "^2.0.1" }, "files": [ "dist", "package.json", - "README.md" + "README.md", + "src/assets/regions.json" ], "devDependencies": { "@nrwl/jest": "^17.3.2", diff --git a/src/assets/regions.json b/src/assets/regions.json new file mode 100644 index 0000000..88a4637 --- /dev/null +++ b/src/assets/regions.json @@ -0,0 +1,186 @@ +{ + "regions": [ + { + "id": "na", + "name": "AWS North America", + "cloudProvider": "AWS", + "location": "North America", + "alias": ["na", "us", "aws-na", "aws_na"], + "isDefault": true, + "endpoints": { + "application": "https://app.contentstack.com", + "contentDelivery": "https://cdn.contentstack.io", + "contentManagement": "https://api.contentstack.io", + "auth": "https://auth-api.contentstack.com", + "graphqlDelivery": "https://graphql.contentstack.com", + "preview": "https://rest-preview.contentstack.com", + "graphqlPreview": "https://graphql-preview.contentstack.com", + "images": "https://images.contentstack.io", + "assets": "https://assets.contentstack.io", + "automate": "https://automations-api.contentstack.com", + "launch": "https://launch-api.contentstack.com", + "developerHub": "https://developerhub-api.contentstack.com", + "brandKit": "https://brand-kits-api.contentstack.com", + "genAI": "https://ai.contentstack.com", + "personalize": "https://personalize-api.contentstack.com", + "personalizeEdge": "https://personalize-edge.contentstack.com" + } + }, + { + "id": "eu", + "name": "AWS Europe", + "cloudProvider": "AWS", + "location": "Europe", + "alias": ["eu", "aws-eu", "aws_eu"], + "isDefault": false, + "endpoints": { + "application": "https://eu-app.contentstack.com", + "contentDelivery": "https://eu-cdn.contentstack.com", + "contentManagement": "https://eu-api.contentstack.com", + "auth": "https://eu-auth-api.contentstack.com", + "graphqlDelivery": "https://eu-graphql.contentstack.com", + "preview": "https://eu-rest-preview.contentstack.com", + "graphqlPreview": "https://eu-graphql-preview.contentstack.com", + "images": "https://eu-images.contentstack.com", + "assets": "https://eu-assets.contentstack.com", + "automate": "https://eu-prod-automations-api.contentstack.com", + "launch": "https://eu-launch-api.contentstack.com", + "developerHub": "https://eu-developerhub-api.contentstack.com", + "brandKit": "https://eu-brand-kits-api.contentstack.com", + "genAI": "https://eu-ai.contentstack.com", + "personalize": "https://eu-personalize-api.contentstack.com", + "personalizeEdge": "https://eu-personalize-edge.contentstack.com" + } + }, + { + "id": "au", + "name": "AWS Australia", + "cloudProvider": "AWS", + "location": "Australia", + "alias": ["au", "aws-au", "aws_au"], + "isDefault": false, + "endpoints": { + "application": "https://au-app.contentstack.com", + "contentDelivery": "https://au-cdn.contentstack.com", + "contentManagement": "https://au-api.contentstack.com", + "auth": "https://au-auth-api.contentstack.com", + "graphqlDelivery": "https://au-graphql.contentstack.com", + "preview": "https://au-rest-preview.contentstack.com", + "graphqlPreview": "https://au-graphql-preview.contentstack.com", + "images": "https://au-images.contentstack.com", + "assets": "https://au-assets.contentstack.com", + "automate": "https://au-prod-automations-api.contentstack.com", + "launch": "https://au-launch-api.contentstack.com", + "developerHub": "https://au-developerhub-api.contentstack.com", + "brandKit": "https://au-brand-kits-api.contentstack.com", + "genAI": "https://au-ai.contentstack.com", + "personalize": "https://au-personalize-api.contentstack.com", + "personalizeEdge": "https://au-personalize-edge.contentstack.com" + } + }, + { + "id": "azure-na", + "name": "Azure North America", + "cloudProvider": "Azure", + "location": "North America", + "alias": ["azure-na", "azure_na"], + "isDefault": false, + "endpoints": { + "application": "https://azure-na-app.contentstack.com", + "contentDelivery": "https://azure-na-cdn.contentstack.com", + "contentManagement": "https://azure-na-api.contentstack.com", + "auth": "https://azure-na-auth-api.contentstack.com", + "graphqlDelivery": "https://azure-na-graphql.contentstack.com", + "preview": "https://azure-na-rest-preview.contentstack.com", + "graphqlPreview": "https://azure-na-graphql-preview.contentstack.com", + "images": "https://azure-na-images.contentstack.com", + "assets": "https://azure-na-assets.contentstack.com", + "automate": "https://azure-na-automations-api.contentstack.com", + "launch": "https://azure-na-launch-api.contentstack.com", + "developerHub": "https://azure-na-developerhub-api.contentstack.com", + "brandKit": "https://azure-na-brand-kits-api.contentstack.com", + "genAI": "https://azure-na-ai.contentstack.com", + "personalize": "https://azure-na-personalize-api.contentstack.com", + "personalizeEdge": "https://azure-na-personalize-edge.contentstack.com" + } + }, + { + "id": "azure-eu", + "name": "Azure Europe", + "cloudProvider": "Azure", + "location": "Europe", + "alias": ["azure-eu", "azure_eu"], + "isDefault": false, + "endpoints": { + "application": "https://azure-eu-app.contentstack.com", + "contentDelivery": "https://azure-eu-cdn.contentstack.com", + "contentManagement": "https://azure-eu-api.contentstack.com", + "auth": "https://azure-eu-auth-api.contentstack.com", + "graphqlDelivery": "https://azure-eu-graphql.contentstack.com", + "preview": "https://azure-eu-rest-preview.contentstack.com", + "graphqlPreview": "https://azure-eu-graphql-preview.contentstack.com", + "images": "https://azure-eu-images.contentstack.com", + "assets": "https://azure-eu-assets.contentstack.com", + "automate": "https://azure-eu-automations-api.contentstack.com", + "launch": "https://azure-eu-launch-api.contentstack.com", + "developerHub": "https://azure-eu-developerhub-api.contentstack.com", + "brandKit": "https://azure-eu-brand-kits-api.contentstack.com", + "genAI": "https://azure-eu-ai.contentstack.com", + "personalize": "https://azure-eu-personalize-api.contentstack.com", + "personalizeEdge": "https://azure-eu-personalize-edge.contentstack.com" + } + }, + { + "id": "gcp-na", + "name": "GCP North America", + "cloudProvider": "GCP", + "location": "North America", + "alias": ["gcp-na", "gcp_na"], + "isDefault": false, + "endpoints": { + "application": "https://gcp-na-app.contentstack.com", + "contentDelivery": "https://gcp-na-cdn.contentstack.com", + "contentManagement": "https://gcp-na-api.contentstack.com", + "auth": "https://gcp-na-auth-api.contentstack.com", + "graphqlDelivery": "https://gcp-na-graphql.contentstack.com", + "preview": "https://gcp-na-rest-preview.contentstack.com", + "graphqlPreview": "https://gcp-na-graphql-preview.contentstack.com", + "images": "https://gcp-na-images.contentstack.com", + "assets": "https://gcp-na-assets.contentstack.com", + "automate": "https://gcp-na-automations-api.contentstack.com", + "launch": "https://gcp-na-launch-api.contentstack.com", + "developerHub": "https://gcp-na-developerhub-api.contentstack.com", + "brandKit": "https://gcp-na-brand-kits-api.contentstack.com", + "genAI": "https://gcp-na-brand-kits-api.contentstack.com", + "personalize": "https://gcp-na-personalize-api.contentstack.com", + "personalizeEdge": "https://gcp-na-personalize-edge.contentstack.com" + } + }, + { + "id": "gcp-eu", + "name": "GCP Europe", + "cloudProvider": "GCP", + "location": "Europe", + "alias": ["gcp-eu", "gcp_eu"], + "isDefault": false, + "endpoints": { + "application": "https://gcp-eu-app.contentstack.com", + "contentDelivery": "https://gcp-eu-cdn.contentstack.com", + "contentManagement": "https://gcp-eu-api.contentstack.com", + "auth": "https://gcp-eu-auth-api.contentstack.com", + "graphqlDelivery": "https://gcp-eu-graphql.contentstack.com", + "preview": "https://gcp-eu-rest-preview.contentstack.com", + "graphqlPreview": "https://gcp-eu-graphql-preview.contentstack.com", + "images": "https://gcp-eu-images.contentstack.com", + "assets": "https://gcp-eu-assets.contentstack.com", + "automate": "https://gcp-eu-automations-api.contentstack.com", + "launch": "https://gcp-eu-launch-api.contentstack.com", + "developerHub": "https://gcp-eu-developerhub-api.contentstack.com", + "brandKit": "https://gcp-eu-brand-kits-api.contentstack.com", + "genAI": "https://gcp-eu-brand-kits-api.contentstack.com", + "personalize": "https://gcp-eu-personalize-api.contentstack.com", + "personalizeEdge": "https://gcp-eu-personalize-edge.contentstack.com" + } + } + ] +} diff --git a/src/lib/contentstack.ts b/src/lib/contentstack.ts index 057a4e8..bce199b 100644 --- a/src/lib/contentstack.ts +++ b/src/lib/contentstack.ts @@ -1,10 +1,11 @@ import { httpClient, retryRequestHandler, retryResponseErrorHandler, retryResponseHandler } from '@contentstack/core'; -import { AxiosRequestHeaders } from 'axios'; +import { AxiosRequestHeaders, InternalAxiosRequestConfig } from 'axios'; import { handleRequest } from './cache'; import { Stack as StackClass } from './stack'; import { Policy, StackConfig, ContentstackPlugin } from './types'; import * as Utility from './utils'; -export * as Utils from '@contentstack/utils'; +import * as Utils from '@contentstack/utils'; +export { Utils }; let version = '{{VERSION}}'; @@ -33,8 +34,10 @@ let version = '{{VERSION}}'; */ // eslint-disable-next-line @typescript-eslint/naming-convention export function stack(config: StackConfig): StackClass { + const DEFAULT_HOST = Utility.getHostforRegion(config.region, config.host); + let defaultConfig = { - defaultHostname: 'cdn.contentstack.io', + defaultHostname: DEFAULT_HOST, headers: {} as AxiosRequestHeaders, params: {} as any, live_preview: {} as any, @@ -42,7 +45,6 @@ export function stack(config: StackConfig): StackClass { ...config }; - defaultConfig.defaultHostname = config.host || Utility.getHost(config.region, config.host); config.host = defaultConfig.defaultHostname; if (config.apiKey) { @@ -104,6 +106,67 @@ export function stack(config: StackConfig): StackClass { }); }; } + // LogHandler interceptors + if (config.debug) { + // Request interceptor for logging + client.interceptors.request.use((requestConfig: any) => { + config.logHandler!('info', { + type: 'request', + method: requestConfig.method?.toUpperCase(), + url: requestConfig.url, + headers: requestConfig.headers, + params: requestConfig.params, + timestamp: new Date().toISOString() + }); + return requestConfig; + }); + + // Response interceptor for logging + client.interceptors.response.use( + (response: any) => { + const level = getLogLevelFromStatus(response.status); + config.logHandler!(level, { + type: 'response', + status: response.status, + statusText: response.statusText, + url: response.config?.url, + method: response.config?.method?.toUpperCase(), + headers: response.headers, + data: response.data, + timestamp: new Date().toISOString() + }); + return response; + }, + (error: any) => { + const status = error.response?.status || 0; + const level = getLogLevelFromStatus(status); + config.logHandler!(level, { + type: 'response_error', + status: status, + statusText: error.response?.statusText || error.message, + url: error.config?.url, + method: error.config?.method?.toUpperCase(), + error: error.message, + timestamp: new Date().toISOString() + }); + throw error; + } + ); + } + + // Helper function to determine log level based on HTTP status code + function getLogLevelFromStatus(status: number): string { + if (status >= 200 && status < 300) { + return 'info'; + } else if (status >= 300 && status < 400) { + return 'warn'; + } else if (status >= 400) { + return 'error'; + } else { + return 'debug'; + } + } + // Retry policy handlers const errorHandler = (error: any) => { return retryResponseErrorHandler(error, config, client); diff --git a/src/lib/stack.ts b/src/lib/stack.ts index 324533d..b4fe63c 100644 --- a/src/lib/stack.ts +++ b/src/lib/stack.ts @@ -8,6 +8,7 @@ import { synchronization } from './synchronization'; import {TaxonomyQuery} from './taxonomy-query'; import { GlobalFieldQuery } from './global-field-query'; import { GlobalField } from './global-field'; +import { getHostforRegion } from './utils'; export class Stack { readonly config: StackConfig; @@ -227,4 +228,20 @@ export class Stack { if (typeof debug === "boolean") this.config.debug = debug; return this; } + + /** + * @method setHost + * @memberOf Stack + * @description Sets the host based on cloud region + * @param {String} cloudRegion - Cloud region (e.g., 'aws_na', 'aws_eu') + * @param {String} host - Optional custom host + * @return {Promise} - Returns the host URL + * @instance + * */ + async setHost(region: string = "aws_na", host?: string): Promise { + const resolvedHost = getHostforRegion(region, host); + + this._client.defaults.baseURL = `https://${resolvedHost}`; + } + } diff --git a/src/lib/types.ts b/src/lib/types.ts index e19a032..4b548c5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -76,7 +76,7 @@ export interface StackConfig extends HttpClientParams { environment: string; branch?: string; early_access?: string[]; - region?: Region; + region?: string; locale?: string; plugins?: ContentstackPlugin[]; logHandler?: (level: string, data: any) => void; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9ed390e..43d4892 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,14 +1,26 @@ import { Region, params } from './types'; +import regionsData from '../assets/regions.json'; -export function getHost(region: Region = Region.US, host?: string) { +export function getHostforRegion(cloudRegion: string = "aws_na", host?: string): string { if (host) return host; - let url = 'cdn.contentstack.io'; - if (region !== Region.US) { - url = region.toString().toLowerCase() + '-cdn.contentstack.com'; + // Handle null, undefined, or empty string cases + if (!cloudRegion || typeof cloudRegion !== 'string') { + throw new Error("Unable to set host using the provided region. Please provide a valid region."); } - return url; + const normalizedRegion = cloudRegion.toLowerCase(); + + const regionObj = regionsData.regions.find(r => + r.id === normalizedRegion || + r.alias.some(alias => alias === normalizedRegion) + ); + + if (!regionObj) { + throw new Error("Unable to set host using the provided region. Please provide a valid region."); + } + + return regionObj ? regionObj.endpoints.contentDelivery.replace(/^https?:\/\//, '') : 'cdn.contentstack.io'; } export function isBrowser() { diff --git a/test/unit/contentstack.spec.ts b/test/unit/contentstack.spec.ts index 2205c66..b65dc46 100644 --- a/test/unit/contentstack.spec.ts +++ b/test/unit/contentstack.spec.ts @@ -1,4 +1,4 @@ -import exp = require("constants"); +import * as exp from "constants"; import * as core from "@contentstack/core"; import * as Contentstack from "../../src/lib/contentstack"; import { Stack } from "../../src/lib/stack"; @@ -9,8 +9,12 @@ import { HOST_AU_REGION, HOST_EU_REGION, HOST_URL, + HOST_AZURE_NA_REGION, + HOST_GCP_NA_REGION, + HOST_GCP_EU_REGION, } from "../utils/constant"; import { AxiosRequestConfig, AxiosResponse } from "axios"; +import * as utils from "../../src/lib/utils"; jest.mock("@contentstack/core"); const createHttpClientMock = >( @@ -257,4 +261,144 @@ describe("Contentstack", () => { createHttpClientMock.mockReset(); done(); }); + + describe('getHostforRegion integration in stack creation', () => { + it('should use getHostforRegion to set default hostname for aws_na region', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "aws_na", + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith("aws_na", undefined); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + + it('should use getHostforRegion to set default hostname for eu region', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "eu", + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith("eu", undefined); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + + it('should use getHostforRegion with custom host when both region and host are provided', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "eu", + host: CUSTOM_HOST, + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith("eu", CUSTOM_HOST); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + + it('should use getHostforRegion for azure-na region', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "azure-na", + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith("azure-na", undefined); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + + it('should use getHostforRegion for gcp-na region', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "gcp-na", + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith("gcp-na", undefined); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + + it('should use getHostforRegion for gcp-eu region', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "gcp-eu", + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith("gcp-eu", undefined); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + + it('should handle getHostforRegion error gracefully', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion').mockImplementation(() => { + throw new Error('Unable to set host using the provided region. Please provide a valid region.'); + }); + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + region: "invalid_region", + }; + + expect(() => createStackInstance(config)).toThrow( + 'Unable to set host using the provided region. Please provide a valid region.' + ); + + getHostforRegionSpy.mockRestore(); + }); + + it('should use getHostforRegion with undefined region when no region is provided', () => { + const getHostforRegionSpy = jest.spyOn(utils, 'getHostforRegion'); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + }; + + const stackInstance = createStackInstance(config); + + expect(getHostforRegionSpy).toHaveBeenCalledWith(undefined, undefined); + expect(stackInstance).toBeInstanceOf(Stack); + + getHostforRegionSpy.mockRestore(); + }); + }); }); diff --git a/test/unit/stack.spec.ts b/test/unit/stack.spec.ts index c3575ae..3312d74 100644 --- a/test/unit/stack.spec.ts +++ b/test/unit/stack.spec.ts @@ -10,6 +10,7 @@ import { synchronization } from '../../src/lib/synchronization'; import { ContentTypeQuery } from '../../src/lib/contenttype-query'; import { AssetQuery } from '../../src/lib/asset-query'; import { StackConfig } from '../../src/lib/types'; +import * as utils from '../../src/lib/utils'; jest.mock('../../src/lib/synchronization'); const syncMock = >(synchronization); @@ -157,5 +158,71 @@ describe('Stack class tests', () => { stack.setPort(3000); expect(stack.config.port).toEqual(3000); }); + + describe('setHost method integration tests', () => { + it('should set baseURL correctly for aws_na region', async () => { + await stack.setHost('aws_na'); + expect(client.defaults.baseURL).toBe('https://cdn.contentstack.io'); + }); + + it('should set baseURL correctly for eu region', async () => { + await stack.setHost('eu'); + expect(client.defaults.baseURL).toBe('https://eu-cdn.contentstack.com'); + }); + + it('should set baseURL correctly for au region', async () => { + await stack.setHost('au'); + expect(client.defaults.baseURL).toBe('https://au-cdn.contentstack.com'); + }); + + it('should set baseURL correctly for azure-na region', async () => { + await stack.setHost('azure-na'); + expect(client.defaults.baseURL).toBe('https://azure-na-cdn.contentstack.com'); + }); + + it('should set baseURL correctly for gcp-na region', async () => { + await stack.setHost('gcp-na'); + expect(client.defaults.baseURL).toBe('https://gcp-na-cdn.contentstack.com'); + }); + + it('should set baseURL correctly for gcp-eu region', async () => { + await stack.setHost('gcp-eu'); + expect(client.defaults.baseURL).toBe('https://gcp-eu-cdn.contentstack.com'); + }); + + it('should prioritize custom host over region', async () => { + const customHost = 'custom.example.com'; + await stack.setHost('eu', customHost); + expect(client.defaults.baseURL).toBe(`https://${customHost}`); + }); + + it('should handle case insensitive regions', async () => { + await stack.setHost('EU'); + expect(client.defaults.baseURL).toBe('https://eu-cdn.contentstack.com'); + }); + + it('should use default region when no region provided', async () => { + await stack.setHost(); + expect(client.defaults.baseURL).toBe('https://cdn.contentstack.io'); + }); + + it('should throw error for invalid region', async () => { + await expect(stack.setHost('invalid_region')).rejects.toThrow( + 'Unable to set host using the provided region. Please provide a valid region.' + ); + }); + + it('should handle region aliases correctly', async () => { + await stack.setHost('na'); + expect(client.defaults.baseURL).toBe('https://cdn.contentstack.io'); + + await stack.setHost('us'); + expect(client.defaults.baseURL).toBe('https://cdn.contentstack.io'); + + await stack.setHost('aws-na'); + expect(client.defaults.baseURL).toBe('https://cdn.contentstack.io'); + }); + }); + }); diff --git a/test/unit/utils.spec.ts b/test/unit/utils.spec.ts index ef2be9b..1703b10 100644 --- a/test/unit/utils.spec.ts +++ b/test/unit/utils.spec.ts @@ -1,5 +1,5 @@ import { Region } from "../../src/lib/types"; -import { getHost } from "../../src/lib/utils"; +import { getHostforRegion, encodeQueryParams } from "../../src/lib/utils"; import { DUMMY_URL, HOST_EU_REGION, @@ -8,11 +8,11 @@ import { HOST_URL, MOCK_CLIENT_OPTIONS, HOST_GCP_EU_REGION, + HOST_AZURE_NA_REGION, } from "../utils/constant"; import { httpClient, AxiosInstance } from "@contentstack/core"; import MockAdapter from "axios-mock-adapter"; import { assetQueryFindResponseDataMock } from "../utils/mocks"; -import { encodeQueryParams } from "../../src/lib/utils"; let client: AxiosInstance; let mockClient: MockAdapter; @@ -22,36 +22,171 @@ beforeAll(() => { mockClient = new MockAdapter(client as any); }); -describe("Utils", () => { - it("should return EU host when region or host is passed", () => { - const url = getHost(Region.EU); - expect(url).toEqual(HOST_EU_REGION); - }); - it("should return AU host when region or host is passed", () => { - const url = getHost(Region.AU); - expect(url).toEqual(HOST_AU_REGION); - }); - it("should return GCP NA host when region or host is passed", () => { - const url = getHost(Region.GCP_NA); - expect(url).toEqual(HOST_GCP_NA_REGION); - }); - it("should return GCP EU host when region or host is passed", () => { - const url = getHost(Region.GCP_EU); - expect(url).toEqual(HOST_GCP_EU_REGION); - }); - it("should return proper US region when nothing is passed", () => { - const url = getHost(); - expect(url).toEqual(HOST_URL); - }); +describe("Utils functions", () => { + describe("getHostforRegion function", () => { + it("should return custom host when provided", () => { + const customHost = "custom.example.com"; + const result = getHostforRegion("aws_na", customHost); + expect(result).toBe(customHost); + }); + + it("should return default host for aws_na region", () => { + const result = getHostforRegion("aws_na"); + expect(result).toBe(HOST_URL); + }); + + it("should return default host when no region is provided", () => { + const result = getHostforRegion(); + expect(result).toBe(HOST_URL); + }); + + it("should return correct host for eu region", () => { + const result = getHostforRegion("eu"); + expect(result).toBe(HOST_EU_REGION); + }); + + it("should return correct host for aws_eu region", () => { + const result = getHostforRegion("aws_eu"); + expect(result).toBe(HOST_EU_REGION); + }); + + it("should return correct host for aws-eu region", () => { + const result = getHostforRegion("aws-eu"); + expect(result).toBe(HOST_EU_REGION); + }); + + it("should return correct host for au region", () => { + const result = getHostforRegion("au"); + expect(result).toBe(HOST_AU_REGION); + }); + + it("should return correct host for aws_au region", () => { + const result = getHostforRegion("aws_au"); + expect(result).toBe(HOST_AU_REGION); + }); + + it("should return correct host for aws-au region", () => { + const result = getHostforRegion("aws-au"); + expect(result).toBe(HOST_AU_REGION); + }); + + it("should return correct host for azure-na region", () => { + const result = getHostforRegion("azure-na"); + expect(result).toBe(HOST_AZURE_NA_REGION); + }); + + it("should return correct host for azure_na region", () => { + const result = getHostforRegion("azure_na"); + expect(result).toBe(HOST_AZURE_NA_REGION); + }); + + it("should return correct host for gcp-na region", () => { + const result = getHostforRegion("gcp-na"); + expect(result).toBe(HOST_GCP_NA_REGION); + }); + + it("should return correct host for gcp_na region", () => { + const result = getHostforRegion("gcp_na"); + expect(result).toBe(HOST_GCP_NA_REGION); + }); + + it("should return correct host for gcp-eu region", () => { + const result = getHostforRegion("gcp-eu"); + expect(result).toBe(HOST_GCP_EU_REGION); + }); + + it("should return correct host for gcp_eu region", () => { + const result = getHostforRegion("gcp_eu"); + expect(result).toBe(HOST_GCP_EU_REGION); + }); + + it("should handle case insensitive region names", () => { + expect(getHostforRegion("AWS_NA")).toBe(HOST_URL); + expect(getHostforRegion("EU")).toBe(HOST_EU_REGION); + expect(getHostforRegion("AU")).toBe(HOST_AU_REGION); + expect(getHostforRegion("AZURE-NA")).toBe(HOST_AZURE_NA_REGION); + expect(getHostforRegion("GCP-NA")).toBe(HOST_GCP_NA_REGION); + }); + + it("should handle mixed case region names", () => { + expect(getHostforRegion("Aws_Na")).toBe(HOST_URL); + expect(getHostforRegion("Eu")).toBe(HOST_EU_REGION); + expect(getHostforRegion("Au")).toBe(HOST_AU_REGION); + expect(getHostforRegion("Azure-Na")).toBe(HOST_AZURE_NA_REGION); + expect(getHostforRegion("Gcp-Na")).toBe(HOST_GCP_NA_REGION); + }); + + it("should throw error for invalid region", () => { + expect(() => getHostforRegion("invalid_region")).toThrow( + "Unable to set host using the provided region. Please provide a valid region." + ); + }); + + it("should throw error for empty string region", () => { + expect(() => getHostforRegion("")).toThrow( + "Unable to set host using the provided region. Please provide a valid region." + ); + }); + + it("should throw error for null region", () => { + expect(() => getHostforRegion(null as any)).toThrow( + "Unable to set host using the provided region. Please provide a valid region." + ); + }); - it("should return the host url if host is passed instead of region", () => { - const host = DUMMY_URL; - const url = getHost(Region.US, host); - expect(url).toEqual(DUMMY_URL); + it("should return default host when undefined region is explicitly passed", () => { + // When undefined is passed explicitly, JavaScript uses the default parameter value "aws_na" + const result = getHostforRegion(undefined as any); + expect(result).toBe(HOST_URL); + }); + + it("should throw error for non-string region types", () => { + expect(() => getHostforRegion(123 as any)).toThrow( + "Unable to set host using the provided region. Please provide a valid region." + ); + + expect(() => getHostforRegion({} as any)).toThrow( + "Unable to set host using the provided region. Please provide a valid region." + ); + + expect(() => getHostforRegion([] as any)).toThrow( + "Unable to set host using the provided region. Please provide a valid region." + ); + }); + + it("should prioritize custom host over region", () => { + const customHost = "priority.example.com"; + const result = getHostforRegion("invalid_region", customHost); + expect(result).toBe(customHost); + }); + + it("should handle region aliases correctly", () => { + // Test all aliases for aws_na + expect(getHostforRegion("na")).toBe(HOST_URL); + expect(getHostforRegion("us")).toBe(HOST_URL); + expect(getHostforRegion("aws-na")).toBe(HOST_URL); + expect(getHostforRegion("aws_na")).toBe(HOST_URL); + }); + + it("should strip protocol from content delivery endpoint", () => { + // The function should remove https:// from the endpoint + const result = getHostforRegion("aws_na"); + expect(result).not.toContain("https://"); + expect(result).not.toContain("http://"); + expect(result).toBe(HOST_URL); + }); + + it("should handle azure-eu region", () => { + const result = getHostforRegion("azure-eu"); + expect(result).toBe("azure-eu-cdn.contentstack.com"); + }); + + it("should handle azure_eu region", () => { + const result = getHostforRegion("azure_eu"); + expect(result).toBe("azure-eu-cdn.contentstack.com"); + }); }); -}); -describe("Utils functions", () => { describe("encodeQueryParams function", () => { it("should encode special characters in strings", () => { const testParams = { diff --git a/tsconfig.json b/tsconfig.json index 4971ec5..2fa7cad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "noPropertyAccessFromIndexSignature": false, "outDir": "dist", "rootDir": ".", + "resolveJsonModule": true, "skipDefaultLibCheck": true, "skipLibCheck": true, "sourceMap": true, diff --git a/tsup.config.js b/tsup.config.js index c1f5834..b2a6de2 100644 --- a/tsup.config.js +++ b/tsup.config.js @@ -1,6 +1,8 @@ import { defineConfig } from 'tsup' import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' import packageJson from './package.json' assert { type: "json" }; +import { copyFileSync, existsSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; export default defineConfig([ modernConfig({ @@ -30,7 +32,21 @@ function modernConfig(opts) { replace: { '{{VERSION}}': `"${packageJson.version}"`, }, - esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'js' })] + esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'js' })], + onSuccess: async () => { + // Copy regions.json to dist/modern/assets/ (industry standard structure) + const sourceFile = 'src/assets/regions.json'; + const targetFile = join('dist/modern/assets', 'regions.json'); + + if (existsSync(sourceFile)) { + const targetDir = dirname(targetFile); + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); + } + copyFileSync(sourceFile, targetFile); + console.log('✓ Copied regions.json to dist/modern/assets'); + } + } } } @@ -54,5 +70,19 @@ function legacyConfig(opts) { options.jsxImportSource = 'preact'; options.jsx = 'automatic' }, + onSuccess: async () => { + // Copy regions.json to dist/legacy/assets/ (industry standard structure) + const sourceFile = 'src/assets/regions.json'; + const targetFile = join('dist/legacy/assets', 'regions.json'); + + if (existsSync(sourceFile)) { + const targetDir = dirname(targetFile); + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); + } + copyFileSync(sourceFile, targetFile); + console.log('✓ Copied regions.json to dist/legacy/assets'); + } + } } } \ No newline at end of file