diff --git a/lib/contentstack.js b/lib/contentstack.js index 9d377999..344898fe 100644 --- a/lib/contentstack.js +++ b/lib/contentstack.js @@ -122,7 +122,7 @@ import { getContentstackEndpoint } from '@contentstack/utils' * const client = contentstack.client({ logHandler: (level, data) => { if (level === 'error' && data) { const title = [data.name, data.message].filter((a) => a).join(' - ') - console.error(`[error] ${title}`) + console.error(`An error occurred due to ${title}. Review the details and try again.`) return } console.log(`[${level}] ${data}`) diff --git a/lib/contentstackClient.js b/lib/contentstackClient.js index b7acdecd..3ca17e9f 100644 --- a/lib/contentstackClient.js +++ b/lib/contentstackClient.js @@ -26,15 +26,15 @@ export default function contentstackClient ({ http }) { * const client = contentstack.client() * * client.login({ email: , password: }) - * .then(() => console.log('Logged in successfully')) + * .then(() => console.log('Login successful.')) * * @example * client.login({ email: , password: , tfa_token: }) - * .then(() => console.log('Logged in successfully')) + * .then(() => console.log('Login successful.')) * * @example * client.login({ email: , password: , mfaSecret: }) - * .then(() => console.log('Logged in successfully')) + * .then(() => console.log('Login successful.')) */ function login (requestBody = {}, params = {}) { http.defaults.versioningStrategy = 'path' @@ -210,7 +210,7 @@ export default function contentstackClient ({ http }) { * const client = contentstack.client() * * client.oauth({ appId: , clientId: , redirectUri: , clientSecret: , responseType: , scope: }) - * .then(() => console.log('Logged in successfully')) + * .then(() => console.log('Login successful.')) * */ function oauth (params = {}) { diff --git a/lib/core/Util.js b/lib/core/Util.js index 23a2f449..446045dd 100644 --- a/lib/core/Util.js +++ b/lib/core/Util.js @@ -1,4 +1,5 @@ import { platform, release } from 'os' +import { ERROR_MESSAGES } from './errorMessages' const HOST_REGEX = /^(?!(?:(?:https?|ftp):\/\/|internal|localhost|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))(?:[\w-]+\.contentstack\.(?:io|com)(?::[^\/\s:]+)?|[\w-]+(?:\.[\w-]+)*(?::[^\/\s:]+)?)(?![\/?#])$/ // eslint-disable-line @@ -218,7 +219,7 @@ const isAllowedHost = (hostname) => { export const validateAndSanitizeConfig = (config) => { if (!config?.url || typeof config?.url !== 'string') { - throw new Error('Invalid request configuration: missing or invalid URL') + throw new Error(ERROR_MESSAGES.INVALID_URL_CONFIG) } // Validate the URL to prevent SSRF attacks diff --git a/lib/core/concurrency-queue.js b/lib/core/concurrency-queue.js index 453e29e5..a9f94a67 100644 --- a/lib/core/concurrency-queue.js +++ b/lib/core/concurrency-queue.js @@ -1,6 +1,7 @@ import Axios from 'axios' import OAuthHandler from './oauthHandler' import { validateAndSanitizeConfig } from './Util' +import { ERROR_MESSAGES } from './errorMessages' const defaultConfig = { maxRequests: 5, @@ -45,23 +46,23 @@ const defaultConfig = { */ export function ConcurrencyQueue ({ axios, config }) { if (!axios) { - throw Error('Axios instance is not present') + throw Error(ERROR_MESSAGES.AXIOS_INSTANCE_MISSING) } if (config) { if (config.maxRequests && config.maxRequests <= 0) { - throw Error('Concurrency Manager Error: minimum concurrent requests is 1') + throw Error(ERROR_MESSAGES.MIN_CONCURRENT_REQUESTS) } else if (config.retryLimit && config.retryLimit <= 0) { - throw Error('Retry Policy Error: minimum retry limit is 1') + throw Error(ERROR_MESSAGES.MIN_RETRY_LIMIT) } else if (config.retryDelay && config.retryDelay < 300) { - throw Error('Retry Policy Error: minimum retry delay for requests is 300') + throw Error(ERROR_MESSAGES.MIN_RETRY_DELAY) } // Validate network retry configuration if (config.maxNetworkRetries && config.maxNetworkRetries < 0) { throw Error('Network Retry Policy Error: maxNetworkRetries cannot be negative') } if (config.networkRetryDelay && config.networkRetryDelay < 50) { - throw Error('Network Retry Policy Error: minimum network retry delay is 50ms') + throw Error(ERROR_MESSAGES.MIN_NETWORK_RETRY_DELAY) } } diff --git a/lib/core/contentstackHTTPClient.js b/lib/core/contentstackHTTPClient.js index ca41ea8c..2851747b 100644 --- a/lib/core/contentstackHTTPClient.js +++ b/lib/core/contentstackHTTPClient.js @@ -3,6 +3,7 @@ import clonedeep from 'lodash/cloneDeep' import Qs from 'qs' import { ConcurrencyQueue } from './concurrency-queue' import { isHost } from './Util' +import { ERROR_MESSAGES } from './errorMessages' export default function contentstackHttpClient (options) { const defaultConfig = { @@ -11,7 +12,7 @@ export default function contentstackHttpClient (options) { logHandler: (level, data) => { if (level === 'error' && data) { const title = [data.name, data.message].filter((a) => a).join(' - ') - console.error(`[error] ${title}`) + console.error(ERROR_MESSAGES.ERROR_WITH_TITLE(title)) return } console.log(`[${level}] ${data}`) diff --git a/lib/core/errorMessages.js b/lib/core/errorMessages.js new file mode 100644 index 00000000..e1a7a8f8 --- /dev/null +++ b/lib/core/errorMessages.js @@ -0,0 +1,40 @@ +/** + * Centralized error messages for the Contentstack Management SDK. + * All user-facing error messages should be defined here for consistency and maintainability. + */ + +export const ERROR_MESSAGES = { + // Asset errors + ASSET_URL_REQUIRED: 'Asset URL is required. Provide a valid asset URL and try again.', + INVALID_UPLOAD_FORMAT: 'Invalid upload format. Provide a valid file path or Buffer and try again.', + + // OAuth errors + OAUTH_BASE_URL_NOT_SET: 'OAuth base URL is not configured. Set the OAuth base URL and try again.', + NO_REFRESH_TOKEN: 'No refresh token available. Authenticate first and try again.', + ACCESS_TOKEN_REQUIRED: 'Access token is required. Provide a valid access token and try again.', + REFRESH_TOKEN_REQUIRED: 'Refresh token is required. Provide a valid refresh token and try again.', + ORGANIZATION_UID_REQUIRED: 'Organization UID is required. Provide a valid organization UID and try again.', + USER_UID_REQUIRED: 'User UID is required. Provide a valid user UID and try again.', + TOKEN_EXPIRY_REQUIRED: 'Token expiry time is required. Provide a valid expiry time and try again.', + AUTH_CODE_NOT_FOUND: 'Authorization code not found in redirect URL. Verify the redirect URL and try again.', + NO_USER_AUTHORIZATIONS: 'No authorizations found for the current user. Verify user permissions and try again.', + NO_APP_AUTHORIZATIONS: 'No authorizations found for the app. Verify app configuration and try again.', + + // Concurrency queue errors + AXIOS_INSTANCE_MISSING: 'Axios instance is not present. Initialize the HTTP client and try again.', + MIN_CONCURRENT_REQUESTS: 'Concurrency Manager Error: Minimum concurrent requests must be at least 1.', + MIN_RETRY_LIMIT: 'Retry Policy Error: Minimum retry limit must be at least 1.', + MIN_RETRY_DELAY: 'Retry Policy Error: Minimum retry delay must be at least 300ms.', + MIN_NETWORK_RETRY_DELAY: 'Network Retry Policy Error: Minimum network retry delay must be at least 50ms.', + + // Request configuration errors + INVALID_URL_CONFIG: 'Invalid request configuration: URL is missing or invalid. Provide a valid URL and try again.', + + // General errors + ERROR_WITH_TITLE: (title) => `An error occurred due to ${title}. Review the details and try again.`, + + // Content type errors + PARAMETER_NAME_REQUIRED: 'Parameter name is required. Provide a valid parameter name and try again.' +} + +export default ERROR_MESSAGES diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index 71cac609..6d64b586 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -1,4 +1,5 @@ import errorFormatter from './contentstackError' +import { ERROR_MESSAGES } from './errorMessages' /** * @description OAuthHandler class to handle OAuth authorization and token management @@ -91,7 +92,7 @@ export default class OAuthHandler { async authorize () { try { if (!this.OAuthBaseURL) { - throw new Error('OAuthBaseURL is not set') + throw new Error(ERROR_MESSAGES.OAUTH_BASE_URL_NOT_SET) } const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize` const authUrl = new URL(baseUrl) @@ -171,7 +172,7 @@ export default class OAuthHandler { const refreshToken = providedRefreshToken || this.axiosInstance.oauth.refreshToken if (!refreshToken) { - throw new Error('No refresh token available. Please authenticate first.') + throw new Error(ERROR_MESSAGES.NO_REFRESH_TOKEN) } const body = new URLSearchParams({ @@ -308,7 +309,7 @@ export default class OAuthHandler { */ setAccessToken (token) { if (!token) { - throw new Error('Access token is required') + throw new Error(ERROR_MESSAGES.ACCESS_TOKEN_REQUIRED) } this.axiosInstance.oauth.accessToken = token } @@ -327,7 +328,7 @@ export default class OAuthHandler { */ setRefreshToken (token) { if (!token) { - throw new Error('Refresh token is required') + throw new Error(ERROR_MESSAGES.REFRESH_TOKEN_REQUIRED) } this.axiosInstance.oauth.refreshToken = token } @@ -346,7 +347,7 @@ export default class OAuthHandler { */ setOrganizationUID (organizationUID) { if (!organizationUID) { - throw new Error('Organization UID is required') + throw new Error(ERROR_MESSAGES.ORGANIZATION_UID_REQUIRED) } this.axiosInstance.oauth.organizationUID = organizationUID } @@ -365,7 +366,7 @@ export default class OAuthHandler { */ setUserUID (userUID) { if (!userUID) { - throw new Error('User UID is required') + throw new Error(ERROR_MESSAGES.USER_UID_REQUIRED) } this.axiosInstance.oauth.userUID = userUID } @@ -384,7 +385,7 @@ export default class OAuthHandler { */ setTokenExpiryTime (expiryTime) { if (!expiryTime) { - throw new Error('Token expiry time is required') + throw new Error(ERROR_MESSAGES.TOKEN_EXPIRY_REQUIRED) } this.axiosInstance.oauth.tokenExpiryTime = expiryTime } @@ -414,7 +415,7 @@ export default class OAuthHandler { errorFormatter(error) } } else { - throw new Error('Authorization code not found in redirect URL.') + throw new Error(ERROR_MESSAGES.AUTH_CODE_NOT_FOUND) } } @@ -443,11 +444,11 @@ export default class OAuthHandler { const userUid = this.axiosInstance.oauth.userUID const currentUserAuthorization = data?.data?.filter((element) => element.user.uid === userUid) || [] if (currentUserAuthorization.length === 0) { - throw new Error('No authorizations found for current user!') + throw new Error(ERROR_MESSAGES.NO_USER_AUTHORIZATIONS) } return currentUserAuthorization[0].authorization_uid // filter authorizations by current logged in user } else { - throw new Error('No authorizations found for the app!') + throw new Error(ERROR_MESSAGES.NO_APP_AUTHORIZATIONS) } } catch (error) { errorFormatter(error) diff --git a/lib/stack/asset/index.js b/lib/stack/asset/index.js index 69e81484..5dc0ec7e 100644 --- a/lib/stack/asset/index.js +++ b/lib/stack/asset/index.js @@ -10,6 +10,7 @@ import { unpublish } from '../../entity' import { Folder } from './folders' import error from '../../core/contentstackError' +import { ERROR_MESSAGES } from '../../core/errorMessages' import FormData from 'form-data' import { createReadStream } from 'fs' @@ -289,7 +290,7 @@ export function Asset (http, data = {}) { } || { responseType } const requestUrl = url || this.url if (!requestUrl || requestUrl === undefined) { - throw new Error('Asset URL can not be empty') + throw new Error(ERROR_MESSAGES.ASSET_URL_REQUIRED) } return http.get(requestUrl, headers) } catch (err) { @@ -338,7 +339,7 @@ export function createFormData (data) { formData.append('asset[upload]', uploadStream) } } else { - throw new Error('Invalid upload format. Must be a file path or Buffer.') + throw new Error(ERROR_MESSAGES.INVALID_UPLOAD_FORMAT) } return formData } diff --git a/lib/stack/contentType/index.js b/lib/stack/contentType/index.js index aaa2b0e5..ffd50f1f 100644 --- a/lib/stack/contentType/index.js +++ b/lib/stack/contentType/index.js @@ -10,6 +10,7 @@ import { } from '../../entity' import { Entry } from './entry/index' import error from '../../core/contentstackError' +import { ERROR_MESSAGES } from '../../core/errorMessages' import FormData from 'form-data' import { createReadStream } from 'fs' @@ -214,7 +215,7 @@ export function ContentType (http, data = {}) { */ this.generateUid = (name) => { if (!name) { - throw new TypeError('Expected parameter name') + throw new TypeError(ERROR_MESSAGES.PARAMETER_NAME_REQUIRED) } return name.replace(/[^A-Z0-9]+/gi, '_').toLowerCase() } diff --git a/test/unit/asset-test.js b/test/unit/asset-test.js index 3970b200..6a7171cb 100644 --- a/test/unit/asset-test.js +++ b/test/unit/asset-test.js @@ -401,7 +401,7 @@ describe('Contentstack Asset test', () => { done() }) .catch((err) => { - expect(err.message).to.be.equal('Asset URL can not be empty') + expect(err.message).to.be.equal('Asset URL is required. Provide a valid asset URL and try again.') done() }) }) @@ -415,7 +415,7 @@ describe('Contentstack Asset test', () => { }) .download({ responseType: 'blob' }) .catch((err) => { - expect(err.message).to.be.equal('Asset URL can not be empty') + expect(err.message).to.be.equal('Asset URL is required. Provide a valid asset URL and try again.') done() }) }) diff --git a/test/unit/concurrency-Queue-test.js b/test/unit/concurrency-Queue-test.js index 6ccf4831..4871fec1 100644 --- a/test/unit/concurrency-Queue-test.js +++ b/test/unit/concurrency-Queue-test.js @@ -189,7 +189,7 @@ describe('Concurrency queue test', () => { new ConcurrencyQueue({ axios: undefined }) expect.fail('Undefined axios should fail') } catch (error) { - expect(error.message).to.be.equal('Axios instance is not present') + expect(error.message).to.be.equal('Axios instance is not present. Initialize the HTTP client and try again.') done() } }) @@ -232,7 +232,7 @@ describe('Concurrency queue test', () => { makeConcurrencyQueue({ maxRequests: -10 }) expect.fail('Negative concurrency queue should fail') } catch (error) { - expect(error.message).to.be.equal('Concurrency Manager Error: minimum concurrent requests is 1') + expect(error.message).to.be.equal('Concurrency Manager Error: Minimum concurrent requests must be at least 1.') done() } }) @@ -242,7 +242,7 @@ describe('Concurrency queue test', () => { makeConcurrencyQueue({ retryLimit: -10 }) expect.fail('Negative retry limit should fail') } catch (error) { - expect(error.message).to.be.equal('Retry Policy Error: minimum retry limit is 1') + expect(error.message).to.be.equal('Retry Policy Error: Minimum retry limit must be at least 1.') done() } }) @@ -252,7 +252,7 @@ describe('Concurrency queue test', () => { makeConcurrencyQueue({ retryDelay: 10 }) expect.fail('Retry delay should be min 300ms') } catch (error) { - expect(error.message).to.be.equal('Retry Policy Error: minimum retry delay for requests is 300') + expect(error.message).to.be.equal('Retry Policy Error: Minimum retry delay must be at least 300ms.') done() } }) diff --git a/test/unit/contentType-test.js b/test/unit/contentType-test.js index a1be2162..6624bb20 100644 --- a/test/unit/contentType-test.js +++ b/test/unit/contentType-test.js @@ -88,7 +88,7 @@ describe('Contentstack ContentType test', () => { it('ContentType generate UID from content type name test', done => { const contentType = makeContentType() - expect(contentType.generateUid.bind(contentType, null)).to.throw('Expected parameter name') + expect(contentType.generateUid.bind(contentType, null)).to.throw('Parameter name is required. Provide a valid parameter name and try again.') expect(contentType.generateUid('Test Name')).to.be.equal('test_name') expect(contentType.generateUid('Test @Name')).to.be.equal('test_name') expect(contentType.generateUid('12 Test Name')).to.be.equal('12_test_name') diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js index 5088a303..e1e774ef 100644 --- a/test/unit/oauthHandler-test.js +++ b/test/unit/oauthHandler-test.js @@ -288,7 +288,7 @@ describe('OAuthHandler', () => { await oauthHandler.getOauthAppAuthorization() throw new Error('Expected error not thrown') } catch (error) { - expect(error.message).to.equal('No authorizations found for current user!') + expect(error.message).to.equal('No authorizations found for the current user. Verify user permissions and try again.') } }) @@ -301,7 +301,7 @@ describe('OAuthHandler', () => { await oauthHandler.getOauthAppAuthorization() throw new Error('Expected error not thrown') } catch (error) { - expect(error.message).to.equal('No authorizations found for the app!') + expect(error.message).to.equal('No authorizations found for the app. Verify app configuration and try again.') } }) })