Skip to content
Merged
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
3 changes: 2 additions & 1 deletion config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ There are a handful of standard config utils that anyone working in this library
#### getConfig()

Locates and parses the hubspot config file. This function will automatically find the correct config file using the following criteria:

1. Checks to see if a config was specified via environment variables (see below)
2. If no environment variables are present, looks for a global config file (located at `~/.hscli/config.yml`)
3. If no global config file is found, looks up the file tree for a local config file.
4. If no local config file is found, throws an error


##### Custom config location environment variables

Comment on lines +16 to +23
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prettier write added these, so I just went with it

- `HUBSPOT_CONFIG_PATH` - specify a file path to a specific config file
- `USE_ENVIRONMENT_HUBSPOT_CONFIG` - load config account from environment variables. The following environment variables are supported:
- `HUBSPOT_PERSONAL_ACCESS_KEY`
Expand Down
2 changes: 1 addition & 1 deletion config/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ describe('config/utils', () => {
{
...PAK_ACCOUNT,
someUndefinedField: undefined,
} as any,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was breaking the build for me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange. I just tested with as any and it still built

},
],
};
const formattedConfig = formatConfigForWrite(configWithUndefined);
Expand Down
20 changes: 14 additions & 6 deletions errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { HubSpotHttpError } from '../models/HubSpotHttpError';
import {
HubSpotHttpError,
HubSpotHttpErrorName,
} from '../models/HubSpotHttpError';
import { BaseError } from '../types/Error';
import { FileSystemError } from '../models/FileSystemError';
import {
FilerSystemErrorName,
FileSystemError,
} from '../models/FileSystemError';
import { HubSpotConfigError } from '../models/HubSpotConfigError';

export function isSpecifiedError(
Expand Down Expand Up @@ -60,7 +66,7 @@ export function isAuthError(err: unknown): err is HubSpotHttpError {
);
}

export function isValidationError(err: unknown): boolean {
export function isValidationError(err: unknown): err is HubSpotHttpError {
return (
isHubSpotHttpError(err) &&
isSpecifiedError(err, { statusCode: 400 }) &&
Expand All @@ -69,10 +75,12 @@ export function isValidationError(err: unknown): boolean {
}

export function isHubSpotHttpError(error?: unknown): error is HubSpotHttpError {
return !!error && error instanceof HubSpotHttpError;
return (
!!error && error instanceof Error && error.name === HubSpotHttpErrorName
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instanceof is not reliable when this package is linked. Comparing the name is a more accurate check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When packages are linked in some places, but not other places, you get multiple versions of the same error class. I.E. if we have LDL linked in the CLI, but not in PPL they will each have their own version of the HubSpotHttpError class that won't match when we run instanceof

);
}

export function isGithubRateLimitError(err: unknown): boolean {
export function isGithubRateLimitError(err: unknown): err is HubSpotHttpError {
if (!isHubSpotHttpError(err)) {
return false;
}
Expand All @@ -96,7 +104,7 @@ export function isSystemError(err: unknown): err is BaseError {
}

export function isFileSystemError(err: unknown): err is FileSystemError {
return err instanceof FileSystemError;
return err instanceof Error && err.name === FilerSystemErrorName;
}

export function isHubSpotConfigError(err: unknown): err is HubSpotConfigError {
Expand Down
4 changes: 3 additions & 1 deletion models/FileSystemError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { isSystemError } from '../errors';

const i18nKey = 'errors.fileSystemErrors';

export const FilerSystemErrorName = 'FilerSystemError';

export class FileSystemError extends Error {
private context: FileSystemErrorContext | undefined;

constructor(options?: ErrorOptions, context?: FileSystemErrorContext) {
super('', options);
this.name = 'FileSystemError';
this.name = FilerSystemErrorName;
this.context = context;

if (context) {
Expand Down
71 changes: 49 additions & 22 deletions models/HubSpotHttpError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { HttpMethod } from '../types/Api';
import { HTTP_METHOD_PREPOSITIONS, HTTP_METHOD_VERBS } from '../constants/api';
import { i18n } from '../utils/lang';

export const HubSpotHttpErrorName = 'HubSpotHttpError';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class HubSpotHttpError<T = any> extends Error {
public status?: number;
Expand All @@ -27,7 +29,7 @@ export class HubSpotHttpError<T = any> extends Error {
context?: HubSpotHttpErrorContext
) {
super(message, options);
this.name = 'HubSpotHttpError';
this.name = HubSpotHttpErrorName;
this.context = context;
this.cause = options?.cause;

Expand Down Expand Up @@ -77,11 +79,18 @@ export class HubSpotHttpError<T = any> extends Error {
}
}

public updateContext(context: Partial<HubSpotHttpErrorContext>) {
public updateContext(
context: Partial<HubSpotHttpErrorContext>,
additionalDebugContext?: string
) {
this.context = { ...this.context, ...context };
// Update the error messages when the context is updated
if (isAxiosError(this.cause)) {
this.message = this.joinErrorMessages(this.cause, this.context);
this.message = this.joinErrorMessages(
this.cause,
this.context,
additionalDebugContext
);
}
}

Expand Down Expand Up @@ -176,9 +185,9 @@ export class HubSpotHttpError<T = any> extends Error {

private joinErrorMessages(
error: AxiosError<{ message: string; errors: { message: string }[] }>,
context: HubSpotHttpErrorContext = {}
context: HubSpotHttpErrorContext = {},
additionalDebugContext?: string
): string {
const i18nKey = 'errors.apiErrors';
const status = error.response?.status;
const method = error.config?.method as HttpMethod;

Expand All @@ -196,57 +205,75 @@ export class HubSpotHttpError<T = any> extends Error {
? `${action} ${preposition} '${context.request}'`
: action;

messageDetail = i18n(`${i18nKey}.messageDetail`, {
messageDetail = i18n(`errors.apiErrors.messageDetail`, {
accountId: context.accountId,
requestName,
});
} else {
messageDetail = i18n(`${i18nKey}.genericMessageDetail`);
messageDetail = i18n(`errors.apiErrors.genericMessageDetail`);
}

const errorMessage: Array<string> = [];

if ((method === 'put' || method === 'post') && context.payload) {
errorMessage.push(
i18n(`${i18nKey}.unableToUpload`, { payload: context.payload })
i18n(`errors.apiErrors.unableToUpload`, { payload: context.payload })
);
}

let statusBasedMessage: string | undefined;

switch (status) {
case 400:
errorMessage.push(i18n(`${i18nKey}.codes.400`, { messageDetail }));
statusBasedMessage = i18n(`errors.apiErrors.codes.400`, {
messageDetail,
});
break;
case 401:
errorMessage.push(i18n(`${i18nKey}.codes.401`, { messageDetail }));
statusBasedMessage = i18n(`errors.apiErrors.codes.401`, {
messageDetail,
});
break;
case 403:
break;
case 404:
errorMessage.push(i18n(`${i18nKey}.codes.404`, { messageDetail }));
statusBasedMessage = i18n(`errors.apiErrors.codes.404`, {
messageDetail,
});
break;
case 429:
errorMessage.push(i18n(`${i18nKey}.codes.429`, { messageDetail }));
statusBasedMessage = i18n(`errors.apiErrors.codes.429`, {
messageDetail,
});
break;
case 503:
errorMessage.push(i18n(`${i18nKey}.codes.503`, { messageDetail }));
statusBasedMessage = i18n(`errors.apiErrors.codes.503`, {
messageDetail,
});
break;
default:
if (status && status >= 500 && status < 600) {
errorMessage.push(
i18n(`${i18nKey}.codes.500Generic`, { messageDetail })
);
statusBasedMessage = i18n(`errors.apiErrors.codes.500Generic`, {
messageDetail,
});
} else if (status && status >= 400 && status < 500) {
errorMessage.push(
i18n(`${i18nKey}.codes.400Generic`, { messageDetail })
);
statusBasedMessage = i18n(`errors.apiErrors.codes.400Generic`, {
messageDetail,
});
} else {
errorMessage.push(
i18n(`${i18nKey}.codes.generic`, { messageDetail })
);
statusBasedMessage = i18n(`errors.apiErrors.codes.generic`, {
messageDetail,
});
}
break;
}

if (statusBasedMessage) {
errorMessage.push(
`${statusBasedMessage}${additionalDebugContext ? ` ${additionalDebugContext}` : ''}`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is additional context, append it to the error message

);
}

if (error?.response?.data) {
const { message, errors } = error.response.data;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,50 @@ describe('models/HubSpotHttpError', () => {
payload: 'payload',
});
});

it('should append additional debug context to the error message when provided', () => {
const cause = newAxiosError();
const hubspotHttpError = new HubSpotHttpError('', { cause });
const additionalDebugContext = 'Extra debug info';
hubspotHttpError.updateContext(
{ accountId: portalId },
additionalDebugContext
);
expect(hubspotHttpError.message).toContain(additionalDebugContext);
});

it('should not modify the error message when additional debug context is not provided', () => {
const cause = newAxiosError();
const hubspotHttpError = new HubSpotHttpError('', { cause });
const originalMessage = hubspotHttpError.message;
hubspotHttpError.updateContext({ accountId: portalId });
expect(hubspotHttpError.message).toBe(originalMessage);
});

it('should handle additional debug context when cause is not an AxiosError', () => {
const hubspotHttpError = new HubSpotHttpError('Test error');
const additionalDebugContext = 'Extra debug info';
expect(() => {
hubspotHttpError.updateContext(
{ accountId: portalId },
additionalDebugContext
);
}).not.toThrow();
expect(hubspotHttpError.context).toStrictEqual({ accountId: portalId });
});

it('should append additional debug context with proper spacing', () => {
const cause = newAxiosError();
const hubspotHttpError = new HubSpotHttpError('', { cause });
const additionalDebugContext = 'Debug: Connection timeout';
hubspotHttpError.updateContext(
{ accountId: portalId },
additionalDebugContext
);
expect(hubspotHttpError.message).toMatch(
/was bad\.\s+Debug: Connection timeout/
);
});
});

describe('toString', () => {
Expand Down