Socket
Socket
Sign inDemoInstall

@hubspot/cms-lib

Package Overview
Dependencies
Maintainers
13
Versions
115
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@hubspot/cms-lib - npm Package Compare versions

Comparing version 2.0.2-beta.2 to 2.1.0

errorHandlers/apiErrors.js

76

__tests__/errorHandlers.js

@@ -0,8 +1,15 @@

const { StatusCodeError } = require('request-promise-native/errors');
const { getPortalConfig } = require('../lib/config');
const { fetchScopeData } = require('../api/localDevAuth/authenticated');
const {
ApiErrorContext,
logApiErrorInstance,
logFileSystemErrorInstance,
logServerlessFunctionApiErrorInstance,
} = require('../errorHandlers');
const { LOG_LEVEL, logger } = require('../logger');
jest.mock('../lib/config');
jest.mock('../logger');
jest.mock('../api/localDevAuth/authenticated');

@@ -182,2 +189,71 @@ function createApiError(statusCode, method) {

});
describe('logServerlessApiErrorInstance', () => {
const portalId = 123;
const logErrorSpy = jest.spyOn(logger, 'error');
beforeEach(() => {
logger.clear();
getPortalConfig.mockReturnValue({
portalId,
authType: 'personalaccesskey',
personalAccessKey: 'let-me-in',
});
});
afterEach(() => {
logErrorSpy.mockReset();
});
it('logs normal errors', async () => {
const message = 'Something went wrong';
const error = new Error(message);
await logServerlessFunctionApiErrorInstance(
portalId,
error,
new ApiErrorContext({
request: 'add secret',
portalId,
})
);
expect(logErrorSpy).toHaveBeenCalledWith(
`A Error has occurred. ${message}`
);
});
it('detects scope error with hub scope', async () => {
const error = new StatusCodeError(403, {
category: 'MISSING_SCOPES',
});
fetchScopeData.mockImplementation(() =>
Promise.resolve({
portalId,
userId: 456,
scopeGroup: 'cms.functions.read_write',
portalScopesInGroup: [
'SERVERLESS_FUNCTIONS',
'SERVERLESS_FUNCTIONS_READ',
'SERVERLESS_FUNCTIONS_WRITE',
],
userScopesInGroup: [
'SERVERLESS_FUNCTIONS',
'SERVERLESS_FUNCTIONS_READ',
'SERVERLESS_FUNCTIONS_WRITE',
],
portalUserScopeIntersection: [
'SERVERLESS_FUNCTIONS',
'SERVERLESS_FUNCTIONS_READ',
'SERVERLESS_FUNCTIONS_WRITE',
],
})
);
await logServerlessFunctionApiErrorInstance(
portalId,
error,
new ApiErrorContext({
request: 'add secret',
portalId,
})
);
expect(logErrorSpy).toHaveBeenCalledWith(
'Your access key does not allow this action. Please generate a new access key by running "hs auth personalaccesskey".'
);
});
});
});

25

__tests__/schema.js

@@ -1,5 +0,9 @@

const { cleanSchema } = require('../schema');
const { cleanSchema, writeSchemaToDisk, logSchemas } = require('../schema');
const basic = require('./fixtures/schema/basic.json');
const full = require('./fixtures/schema/full.json');
const { getCwd } = require('@hubspot/cms-lib/path');
const multiple = require('./fixtures/schema/multiple.json');
const fs = require('fs-extra');
const path = require('path');
const { logger } = require('@hubspot/cms-lib/logger');

@@ -20,2 +24,21 @@ describe('cms-lib/schema', () => {

});
describe('writeSchemaToDisk()', () => {
it('writes schema to disk', () => {
const spy = jest.spyOn(fs, 'outputFileSync');
expect(fs.existsSync(path.resolve(getCwd(), `${basic.name}.json`))).toBe(
false
);
writeSchemaToDisk(basic);
expect(spy.mock.calls[0][1]).toMatchSnapshot();
});
});
describe('logSchemas()', () => {
it('logs schemas', () => {
const spy = jest.spyOn(logger, 'log');
logSchemas([basic]);
expect(spy.mock.calls[0][0]).toMatchSnapshot();
});
});
});

360

errorHandlers.js

@@ -1,346 +0,18 @@

const { HubSpotAuthError } = require('./lib/models/Errors');
const { logger } = require('./logger');
const {
ErrorContext,
isFatalError,
logErrorInstance,
} = require('./errorHandlers/standardErrors');
const {
FileSystemErrorContext,
logFileSystemErrorInstance,
} = require('./errorHandlers/fileSystemErrors');
const {
ApiErrorContext,
logApiErrorInstance,
logApiUploadErrorInstance,
logServerlessFunctionApiErrorInstance,
parseValidationErrors,
} = require('./errorHandlers/apiErrors');
const isApiStatusCodeError = err =>
err.name === 'StatusCodeError' ||
(err.statusCode >= 100 && err.statusCode < 600);
const isApiUploadValidationError = err =>
!!(
err.statusCode === 400 &&
err.response &&
err.response.body &&
(err.response.body.message || err.response.body.errors)
);
const isSystemError = err =>
err.errno != null && err.code != null && err.syscall != null;
const isFatalError = err => err instanceof HubSpotAuthError;
const isMissingScopeError = err =>
err.name === 'StatusCodeError' &&
err.statusCode === 403 &&
err.error.category === 'MISSING_SCOPES';
const contactSupportString =
'Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.';
const parseValidationErrors = (responseBody = {}) => {
const errorMessages = [];
const { errors, message } = responseBody;
if (message) {
errorMessages.push(message);
}
if (errors) {
const specificErrors = errors.map(error => {
let errorMessage = error.message;
if (error.errorTokens && error.errorTokens.line) {
errorMessage = `line ${error.errorTokens.line}: ${errorMessage}`;
}
return errorMessage;
});
errorMessages.push(...specificErrors);
}
return errorMessages;
};
// TODO: Make these TS interfaces
class ErrorContext {
constructor(props = {}) {
/** @type {number} */
this.portalId = props.portalId;
}
}
class ApiErrorContext extends ErrorContext {
constructor(props = {}) {
super(props);
/** @type {string} */
this.request = props.request || '';
/** @type {string} */
this.payload = props.payload || '';
}
}
class FileSystemErrorContext extends ErrorContext {
constructor(props = {}) {
super(props);
/** @type {string} */
this.filepath = props.filepath || '';
/** @type {boolean} */
this.read = !!props.read;
/** @type {boolean} */
this.write = !!props.write;
}
}
/**
* Logs (debug) the error and context objects.
*
* @param {SystemError} error
* @param {ErrorContext} context
*/
function debugErrorAndContext(error, context) {
if (error.name === 'StatusCodeError') {
const { statusCode, message, response } = error;
logger.debug('Error: %o', {
statusCode,
message,
url: response.request.href,
method: response.request.method,
response: response.body,
headers: response.headers,
});
} else {
logger.debug('Error: %o', error);
}
logger.debug('Context: %o', context);
}
/**
* Logs a SystemError
* @see {@link https://nodejs.org/api/errors.html#errors_class_systemerror}
*
* @param {SystemError} error
* @param {ErrorContext} context
*/
function logSystemError(error, context) {
logger.error(`A system error has occurred: ${error.message}`);
debugErrorAndContext(error, context);
}
/**
* Logs a message for an error instance of type not asserted.
*
* @param {Error|SystemError|Object} error
* @param {ErrorContext} context
*/
function logErrorInstance(error, context) {
// SystemError
if (isSystemError(error)) {
logSystemError(error, context);
return;
}
if (error instanceof Error || error.message || error.reason) {
// Error or Error subclass
const name = error.name || 'Error';
const message = [`A ${name} has occurred.`];
[error.message, error.reason].forEach(msg => {
if (msg) {
message.push(msg);
}
});
logger.error(message.join(' '));
} else {
// Unknown errors
logger.error(`An unknown error has occurred.`);
}
debugErrorAndContext(error, context);
}
/**
* @param {Error} error
* @param {ApiErrorContext} context
*/
function logValidationErrors(error, context) {
const { response = {} } = error;
const validationErrors = parseValidationErrors(response.body);
if (validationErrors.length) {
validationErrors.forEach(err => {
logger.error(err);
});
}
debugErrorAndContext(error, context);
}
/**
* Message segments for API messages.
*
* @enum {string}
*/
const ApiMethodVerbs = {
DEFAULT: 'request',
DELETE: 'delete',
GET: 'request',
PATCH: 'update',
POST: 'post',
PUT: 'update',
};
/**
* Message segments for API messages.
*
* @enum {string}
*/
const ApiMethodPrepositions = {
DEFAULT: 'for',
DELETE: 'of',
GET: 'for',
PATCH: 'to',
POST: 'to',
PUT: 'to',
};
/**
* Logs messages for an error instance resulting from API interaction.
*
* @param {StatusCodeError} error
* @param {ApiErrorContext} context
*/
function logApiStatusCodeError(error, context) {
const { statusCode } = error;
const { method } = error.options || {};
const isPutOrPost = method === 'PUT' || method === 'POST';
const action = ApiMethodVerbs[method] || ApiMethodVerbs.DEFAULT;
const preposition =
ApiMethodPrepositions[method] || ApiMethodPrepositions.DEFAULT;
let messageDetail = '';
{
const request = context.request
? `${action} ${preposition} "${context.request}"`
: action;
messageDetail = `${request} in portal ${context.portalId}`;
}
const errorMessage = [];
if (isPutOrPost && context.payload) {
errorMessage.push(`Unable to upload "${context.payload}".`);
}
switch (statusCode) {
case 400:
errorMessage.push(`The ${messageDetail} was bad.`);
break;
case 401:
errorMessage.push(`The ${messageDetail} was unauthorized.`);
break;
case 403:
errorMessage.push(`The ${messageDetail} was forbidden.`);
break;
case 404:
if (context.request) {
errorMessage.push(
`The ${action} failed because "${context.request}" was not found in portal ${context.portalId}.`
);
} else {
errorMessage.push(`The ${messageDetail} was not found.`);
}
break;
case 503:
errorMessage.push(
`The ${messageDetail} could not be handled at this time. ${contactSupportString}`
);
break;
default:
if (statusCode >= 500 && statusCode < 600) {
errorMessage.push(
`The ${messageDetail} failed due to a server error. ${contactSupportString}`
);
} else if (statusCode >= 400 && statusCode < 500) {
errorMessage.push(`The ${messageDetail} failed due to a client error.`);
} else {
errorMessage.push(`The ${messageDetail} failed.`);
}
break;
}
if (error.error && error.error.message) {
errorMessage.push(error.error.message);
}
logger.error(errorMessage.join(' '));
debugErrorAndContext(error, context);
}
/**
* Logs a message for an error instance resulting from API interaction.
*
* @param {Error|SystemError|Object} error
* @param {ApiErrorContext} context
*/
function logApiErrorInstance(error, context) {
// StatusCodeError
if (isApiStatusCodeError(error)) {
logApiStatusCodeError(error, context);
return;
}
logErrorInstance(error, context);
}
/**
* Logs a message for an error instance resulting from filemapper API upload.
*
* @param {Error|SystemError|Object} error
* @param {ApiErrorContext} context
*/
function logApiUploadErrorInstance(error, context) {
if (isApiUploadValidationError(error)) {
logValidationErrors(error, context);
return;
}
logApiErrorInstance(error, context);
}
/**
* Logs a message for an error instance resulting from filesystem interaction.
*
* @param {Error|SystemError|Object} error
* @param {FileSystemErrorContext} context
*/
function logFileSystemErrorInstance(error, context) {
let fileAction = '';
if (context.read) {
fileAction = 'reading from';
} else if (context.write) {
fileAction = 'writing to';
} else {
fileAction = 'accessing';
}
const filepath = context.filepath
? `"${context.filepath}"`
: 'a file or folder';
const message = [`An error occurred while ${fileAction} ${filepath}.`];
// Many `fs` errors will be `SystemError`s
if (isSystemError(error)) {
message.push(`This is the result of a system error: ${error.message}`);
}
logger.error(message.join(' '));
debugErrorAndContext(error, context);
}
/**
* Logs a message for an error instance resulting from API interaction
* related to serverless function.
*
* @param {Error|SystemError|Object} error
* @param {ApiErrorContext} context
*/
function logServerlessFunctionApiErrorInstance(error, scopesData, context) {
if (isMissingScopeError(error) && scopesData) {
const { portalScopesInGroup, userScopesInGroup } = scopesData;
if (!portalScopesInGroup.length) {
logger.error(
'Your account does not have access to this action. Talk to an account admin to request it.'
);
return;
}
if (!portalScopesInGroup.every(s => userScopesInGroup.includes(s))) {
logger.error(
"You don't have access to this action. Ask an account admin to change your permissions in Users & Teams settings."
);
return;
} else {
logger.error(
'Your access key does not allow this action. Please generate a new access key by running "hs auth personalaccesskey".'
);
return;
}
}
// StatusCodeError
if (isApiStatusCodeError(error)) {
logApiStatusCodeError(error, context);
return;
}
logErrorInstance(error, context);
}
module.exports = {

@@ -347,0 +19,0 @@ ErrorContext,

@@ -9,5 +9,5 @@ const fs = require('fs-extra');

const {
logErrorInstance,
logFileSystemErrorInstance,
} = require('../errorHandlers');
} = require('../errorHandlers/fileSystemErrors');
const { logErrorInstance } = require('../errorHandlers/standardErrors');
const { getCwd } = require('../path');

@@ -14,0 +14,0 @@ const {

@@ -18,2 +18,3 @@ const request = require('request-promise-native');

tokenInfo = { expiresAt: null, refreshToken: null, accessToken: null },
name,
},

@@ -32,2 +33,3 @@ logger = console,

this.refreshTokenRequest = null;
this.name = name;
}

@@ -128,2 +130,3 @@

tokenInfo: this.tokenInfo,
name: this.name,
};

@@ -130,0 +133,0 @@ }

{
"name": "@hubspot/cms-lib",
"version": "2.0.2-beta.2",
"version": "2.1.0",
"description": "Library for working with the HubSpot CMS",

@@ -34,3 +34,3 @@ "license": "Apache-2.0",

},
"gitHead": "d352c878526fee39e59b59d6cda43e48840fe313"
"gitHead": "31128283cf58dacc24a9a1668d3e25b7eb75c353"
}

@@ -15,4 +15,4 @@ const moment = require('moment');

} = require('./lib/constants');
const { logErrorInstance } = require('./errorHandlers/standardErrors');
const { fetchAccessToken } = require('./api/localDevAuth/unauthenticated');
const { logErrorInstance } = require('./errorHandlers');

@@ -19,0 +19,0 @@ const refreshRequests = new Map();

@@ -50,5 +50,11 @@ const fs = require('fs-extra');

const getResolvedPath = (dest, name) => {
if (name) return path.resolve(getCwd(), dest || '', `${name}.json`);
return path.resolve(getCwd(), dest || '');
};
const writeSchemaToDisk = (schema, dest) =>
fs.outputFileSync(
path.resolve(getCwd(), dest || '', `${schema.name}.json`),
getResolvedPath(dest, schema.name),
prettier.format(JSON.stringify(cleanSchema(schema)), {

@@ -70,4 +76,5 @@ parser: 'json',

response.results.forEach(r => writeSchemaToDisk(r, dest));
logger.log(`Wrote schemas to ${path.resolve(getCwd(), dest || '')}`);
}
return;
};

@@ -82,2 +89,3 @@

writeSchemaToDisk,
getResolvedPath,
listSchemas,

@@ -87,2 +95,3 @@ cleanSchema,

downloadSchema,
logSchemas,
};

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc