Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default class ContentTypesExport extends BaseClass {
}

sanitizeAttribs(contentTypes: Record<string, unknown>[]): Record<string, unknown>[] {
log.debug(`Sanitizing ${contentTypes.length} content types`, this.exportConfig.context);
log.debug(`Sanitizing ${contentTypes?.length} content types`, this.exportConfig.context);

const updatedContentTypes: Record<string, unknown>[] = [];

Expand All @@ -121,7 +121,7 @@ export default class ContentTypesExport extends BaseClass {
}

async writeContentTypes(contentTypes: Record<string, unknown>[]) {
log.debug(`Writing ${contentTypes.length} content types to disk`, this.exportConfig.context);
log.debug(`Writing ${contentTypes?.length} content types to disk`, this.exportConfig.context);

function write(contentType: Record<string, unknown>) {
return fsUtil.writeFile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default class ExportCustomRoles extends BaseClass {
await this.getLocales();
await this.getCustomRolesLocales();

log.debug(`Custom roles export completed. Total custom roles: ${Object.keys(this.customRoles).length}`, this.exportConfig.context);
log.debug(`Custom roles export completed. Total custom roles: ${Object.keys(this.customRoles)?.length}`, this.exportConfig.context);
}

async getCustomRoles(): Promise<void> {
Expand All @@ -75,8 +75,8 @@ export default class ExportCustomRoles extends BaseClass {
}

customRoles.forEach((role: any) => {
log.debug(`Processing custom role: ${role.name} (${role.uid})`, this.exportConfig.context);
log.info(messageHandler.parse('ROLES_EXPORTING_ROLE', role.name), this.exportConfig.context);
log.debug(`Processing custom role: ${role?.name} (${role?.uid})`, this.exportConfig.context);
log.info(messageHandler.parse('ROLES_EXPORTING_ROLE', role?.name), this.exportConfig.context);
this.customRoles[role.uid] = role;
});

Expand All @@ -93,7 +93,7 @@ export default class ExportCustomRoles extends BaseClass {
.query({})
.find()
.then((data: any) => {
log.debug(`Fetched ${data.items?.length || 0} locales`, this.exportConfig.context);
log.debug(`Fetched ${data?.items?.length || 0} locales`, this.exportConfig.context);
return data;
})
.catch((err: any) => {
Expand All @@ -102,23 +102,23 @@ export default class ExportCustomRoles extends BaseClass {
});

for (const locale of locales.items) {
log.debug(`Mapping locale: ${locale.name} (${locale.uid})`, this.exportConfig.context);
log.debug(`Mapping locale: ${locale?.name} (${locale?.uid})`, this.exportConfig.context);
this.sourceLocalesMap[locale.uid] = locale;
}

log.debug(`Mapped ${Object.keys(this.sourceLocalesMap).length} locales`, this.exportConfig.context);
log.debug(`Mapped ${Object.keys(this.sourceLocalesMap)?.length} locales`, this.exportConfig.context);
}

async getCustomRolesLocales() {
log.debug('Processing custom roles locales mapping...', this.exportConfig.context);

for (const role of values(this.customRoles)) {
const customRole = role as Record<string, any>;
log.debug(`Processing locales for custom role: ${customRole.name}`, this.exportConfig.context);
log.debug(`Processing locales for custom role: ${customRole?.name}`, this.exportConfig.context);

const rulesLocales = find(customRole.rules, (rule: any) => rule.module === 'locale');
if (rulesLocales?.locales?.length) {
log.debug(`Found ${rulesLocales.locales.length} locales for role: ${customRole.name}`, this.exportConfig.context);
log.debug(`Found ${rulesLocales.locales.length} locales for role: ${customRole?.name}`, this.exportConfig.context);
forEach(rulesLocales.locales, (locale: any) => {
log.debug(`Adding locale ${locale} to custom roles mapping`, this.exportConfig.context);
this.localesMap[locale] = 1;
Expand All @@ -127,7 +127,7 @@ export default class ExportCustomRoles extends BaseClass {
}

if (keys(this.localesMap)?.length) {
log.debug(`Processing ${keys(this.localesMap).length} custom role locales`, this.exportConfig.context);
log.debug(`Processing ${keys(this.localesMap)?.length} custom role locales`, this.exportConfig.context);

for (const locale in this.localesMap) {
if (this.sourceLocalesMap[locale] !== undefined) {
Expand Down
4 changes: 2 additions & 2 deletions packages/contentstack-export/src/export/modules/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ export default class EntriesExport extends BaseClass {
try {
log.debug('Starting entries export process...', this.exportConfig.context);
const locales = fsUtil.readFile(this.localesFilePath) as Array<Record<string, unknown>>;
log.debug(`Loaded ${locales.length} locales from ${this.localesFilePath}`, this.exportConfig.context);
log.debug(`Loaded ${locales?.length} locales from ${this.localesFilePath}`, this.exportConfig.context);

const contentTypes = fsUtil.readFile(this.schemaFilePath) as Array<Record<string, unknown>>;
log.debug(`Loaded ${contentTypes.length} content types from ${this.schemaFilePath}`, this.exportConfig.context);
log.debug(`Loaded ${contentTypes?.length} content types from ${this.schemaFilePath}`, this.exportConfig.context);

if (contentTypes.length === 0) {
log.info(messageHandler.parse('CONTENT_TYPE_NO_TYPES'), this.exportConfig.context);
Expand Down
39 changes: 37 additions & 2 deletions packages/contentstack-utilities/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { checkSync } from 'recheck';
import traverse from 'traverse';
import authHandler from './auth-handler';
import { HttpClient, cliux, configHandler } from '.';

export const isAuthenticated = () => authHandler.isAuthenticated();
export const doesBranchExist = async (stack, branchName) => {
return stack
Expand Down Expand Up @@ -92,9 +93,38 @@ export const formatError = function (error: any) {
parsedError = error;
}

// Check if parsedError is an empty object
if (parsedError && typeof parsedError === 'object' && Object.keys(parsedError).length === 0) {
return `An unknown error occurred. ${error}`;
if (
!parsedError.message &&
!parsedError.code &&
!parsedError.status &&
!parsedError.errorMessage &&
!parsedError.error_message
) {
return `An unknown error occurred. ${error}`;
}
}

if (parsedError?.response?.data?.errorMessage) {
return parsedError.response.data.errorMessage;
}

if (parsedError?.errorMessage) {
return parsedError.errorMessage;
}

const status = parsedError?.status || parsedError?.response?.status;
const errorCode = parsedError?.errorCode || parsedError?.response?.data?.errorCode;
if (status === 422 && errorCode === 104) {
return 'Invalid email or password. Please check your credentials and try again.';
}

if (status === 401) {
return 'Authentication failed. Please check your credentials.';
}

if (status === 403) {
return 'Access denied. Please check your permissions.';
}

// Check for specific SSL error
Expand All @@ -107,6 +137,11 @@ export const formatError = function (error: any) {
return 'Self-signed certificate in the certificate chain! Please ensure your certificate configuration is correct and the necessary CA certificates are trusted.';
}

// ENHANCED: Handle network errors with user-friendly messages
if (['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ENETUNREACH'].includes(parsedError?.code)) {
return `Connection failed: Unable to reach the server. Please check your internet connection.`;
}

// Determine the error message
let message =
parsedError.errorMessage || parsedError.error_message || parsedError?.code || parsedError.message || parsedError;
Expand Down
62 changes: 39 additions & 23 deletions packages/contentstack-utilities/src/logger/cli-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AxiosError } from 'axios';
import { ClassifiedError, ErrorContext } from '../interfaces';
import { formatError } from '../helpers';
import { ERROR_TYPES } from '../constants/errorTypes';
import { redactObject } from '../helpers';

/**
* Handles errors in a CLI application by classifying, normalizing, and extracting
Expand Down Expand Up @@ -84,19 +85,22 @@ export default class CLIErrorHandler {
* Extracts a clear, concise error message from various error types.
*/
private extractClearMessage(error: Error & Record<string, any>): string {
const { message, code, status } = error;

// For API errors, include status code for clarity
if (status && status >= 400) {
return `${message} (HTTP ${status})`;
if (error?.response?.data?.errorMessage) {
return error.response.data.errorMessage;
}

// For network errors, include error code
if (code && ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ENETUNREACH'].includes(code)) {
return `${message} (${code})`;
if (error?.errorMessage) {
return error.errorMessage;
}

return message || 'Unknown error occurred';
// Use existing formatError function for other cases
try {
const formattedMessage = formatError(error);

return formattedMessage || 'An error occurred. Please try again.';
} catch {
return 'An error occurred. Please try again.';
}
}

/**
Expand Down Expand Up @@ -168,37 +172,45 @@ export default class CLIErrorHandler {

const payload: Record<string, any> = {
name,
message: formatError(error),
message: this.extractClearMessage(error),
};

// Add error identifiers
if (code) payload.code = code;
if (status || response?.status) payload.status = status || response?.status;

// Add request context (only essential info)
// Add request context with sensitive data redaction
if (request || config) {
const method = request?.method || config?.method;
const url = request?.url || config?.url;

payload.request = {
method,
url,
const requestData = {
method: request?.method || config?.method,
url: request?.url || config?.url,
headers: request?.headers || config?.headers,
data: request?.data || config?.data,
timeout: config?.timeout,
baseURL: config?.baseURL,
params: config?.params,
};

// Use existing redactObject to mask sensitive data
payload.request = redactObject(requestData);
}

// Add response context (only essential info)
// Add response context with sensitive data redaction
if (response) {
payload.response = {
const responseData = {
status,
statusText,
headers: error.response?.headers,
data: error.response?.data,
headers: response.headers,
data: response.data,
};

// Use existing redactObject to mask sensitive data
payload.response = redactObject(responseData);
}

// Extract user-friendly error message for API errors
if (response?.data?.errorMessage) {
payload.userFriendlyMessage = response.data.errorMessage;
}

// Add stack trace only for non-API errors to avoid clutter
Expand All @@ -207,7 +219,7 @@ export default class CLIErrorHandler {
this.determineErrorType(error) as typeof ERROR_TYPES.API_ERROR | typeof ERROR_TYPES.SERVER_ERROR,
)
) {
payload.stack = error.stack?.split('\n').slice(0, 5).join('\n'); // Limit stack trace
payload.stack = error.stack?.split('\n').slice(0, 5).join('\n');
}

return payload;
Expand Down Expand Up @@ -254,9 +266,13 @@ export default class CLIErrorHandler {
'api-key',
'authorization',
'sessionid',
'email',
'authtoken',
'x-api-key',
'tfa_token',
'otp',
'security_code',
'bearer',
'cookie',
];

return sensitiveTerms.some((term) => content.includes(term));
Expand Down
Loading