Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/contentstack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
8 changes: 4 additions & 4 deletions lib/contentstackClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ export default function contentstackClient ({ http }) {
* const client = contentstack.client()
*
* client.login({ email: <emailid>, password: <password> })
* .then(() => console.log('Logged in successfully'))
* .then(() => console.log('Login successful.'))
*
* @example
* client.login({ email: <emailid>, password: <password>, tfa_token: <tfa_token> })
* .then(() => console.log('Logged in successfully'))
* .then(() => console.log('Login successful.'))
*
* @example
* client.login({ email: <emailid>, password: <password>, mfaSecret: <mfa_secret> })
* .then(() => console.log('Logged in successfully'))
* .then(() => console.log('Login successful.'))
*/
function login (requestBody = {}, params = {}) {
http.defaults.versioningStrategy = 'path'
Expand Down Expand Up @@ -210,7 +210,7 @@ export default function contentstackClient ({ http }) {
* const client = contentstack.client()
*
* client.oauth({ appId: <appId>, clientId: <clientId>, redirectUri: <redirectUri>, clientSecret: <clientSecret>, responseType: <responseType>, scope: <scope> })
* .then(() => console.log('Logged in successfully'))
* .then(() => console.log('Login successful.'))
*
*/
function oauth (params = {}) {
Expand Down
3 changes: 2 additions & 1 deletion lib/core/Util.js
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/core/contentstackHTTPClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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}`)
Expand Down
40 changes: 40 additions & 0 deletions lib/core/errorMessages.js
Original file line number Diff line number Diff line change
@@ -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
21 changes: 11 additions & 10 deletions lib/core/oauthHandler.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import errorFormatter from './contentstackError'
import { ERROR_MESSAGES } from './errorMessages'

/**
* @description OAuthHandler class to handle OAuth authorization and token management
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions lib/stack/asset/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion lib/stack/contentType/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions test/unit/asset-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
Expand All @@ -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()
})
})
Expand Down
8 changes: 4 additions & 4 deletions test/unit/concurrency-Queue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
Expand Down Expand Up @@ -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()
}
})
Expand All @@ -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()
}
})
Expand All @@ -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()
}
})
Expand Down
2 changes: 1 addition & 1 deletion test/unit/contentType-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions test/unit/oauthHandler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
}
})

Expand All @@ -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.')
}
})
})
Expand Down
Loading