Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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