-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
479 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,343 @@ | ||
const { logger } = require('@hubspot/cli-lib/logger'); | ||
const { getAccountConfig } = require('@hubspot/cli-lib/lib/config'); | ||
const { | ||
SCOPE_GROUPS, | ||
PERSONAL_ACCESS_KEY_AUTH_METHOD, | ||
} = require('@hubspot/cli-lib/lib/constants'); | ||
const { | ||
fetchScopeData, | ||
} = require('@hubspot/cli-lib/api/localDevAuth/authenticated'); | ||
const { | ||
debugErrorAndContext, | ||
logErrorInstance, | ||
ErrorContext, | ||
} = require('./standardErrors'); | ||
|
||
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 isMissingScopeError = err => | ||
err.name === 'StatusCodeError' && | ||
err.statusCode === 403 && | ||
err.error && | ||
err.error.category === 'MISSING_SCOPES'; | ||
|
||
const isGatingError = err => | ||
isSpecifiedError(err, { statusCode: 403, category: 'GATED' }); | ||
|
||
const isSpecifiedError = (err, { statusCode, category, subCategory } = {}) => { | ||
const statusCodeErr = !statusCode || err.statusCode === statusCode; | ||
const categoryErr = | ||
!category || (err.error && err.error.category === category); | ||
const subCategoryErr = | ||
!subCategory || (err.error && err.error.subCategory === subCategory); | ||
|
||
return ( | ||
err.name === 'StatusCodeError' && | ||
statusCodeErr && | ||
categoryErr && | ||
subCategoryErr | ||
); | ||
}; | ||
|
||
const isSpecifiedHubSpotAuthError = ( | ||
err, | ||
{ statusCode, category, subCategory } | ||
) => { | ||
const statusCodeErr = !statusCode || err.statusCode === statusCode; | ||
const categoryErr = !category || err.category === category; | ||
const subCategoryErr = !subCategory || err.subCategory === subCategory; | ||
return ( | ||
err.name === 'HubSpotAuthError' && | ||
statusCodeErr && | ||
categoryErr && | ||
subCategoryErr | ||
); | ||
}; | ||
|
||
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; | ||
}; | ||
|
||
class ApiErrorContext extends ErrorContext { | ||
constructor(props = {}) { | ||
super(props); | ||
/** @type {string} */ | ||
this.request = props.request || ''; | ||
/** @type {string} */ | ||
this.payload = props.payload || ''; | ||
/** @type {string} */ | ||
this.projectName = props.projectName || ''; | ||
} | ||
} | ||
|
||
/** | ||
* @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 { projectName } = context; | ||
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 account ${context.accountId}`; | ||
} | ||
const errorMessage = []; | ||
if (isPutOrPost && context.payload) { | ||
errorMessage.push(`Unable to upload "${context.payload}".`); | ||
} | ||
const isProjectMissingScopeError = isMissingScopeError(error) && projectName; | ||
const isProjectGatingError = isGatingError(error) && projectName; | ||
switch (statusCode) { | ||
case 400: | ||
errorMessage.push(`The ${messageDetail} was bad.`); | ||
break; | ||
case 401: | ||
errorMessage.push(`The ${messageDetail} was unauthorized.`); | ||
break; | ||
case 403: | ||
if (isProjectMissingScopeError) { | ||
errorMessage.push( | ||
`Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for ${context.accountId}, and generate a new one. Then run \`hs auth\` to update the CLI with the new key.` | ||
); | ||
} else if (isProjectGatingError) { | ||
errorMessage.push( | ||
`The current target account ${context.accountId} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216.` | ||
); | ||
} else { | ||
errorMessage.push(`The ${messageDetail} was forbidden.`); | ||
} | ||
break; | ||
case 404: | ||
if (context.request) { | ||
errorMessage.push( | ||
`The ${action} failed because "${context.request}" was not found in account ${context.accountId}.` | ||
); | ||
} else { | ||
errorMessage.push(`The ${messageDetail} was not found.`); | ||
} | ||
break; | ||
case 429: | ||
errorMessage.push( | ||
`The ${messageDetail} surpassed the rate limit. Retry in one minute.` | ||
); | ||
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 && | ||
!isProjectMissingScopeError && | ||
!isProjectGatingError | ||
) { | ||
errorMessage.push(error.error.message); | ||
} | ||
if (error.error && error.error.errors) { | ||
error.error.errors.forEach(err => { | ||
errorMessage.push('\n- ' + err.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); | ||
} | ||
|
||
async function verifyAccessKeyAndUserAccess(accountId, scopeGroup) { | ||
const accountConfig = getAccountConfig(accountId); | ||
const { authType } = accountConfig; | ||
if (authType !== PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { | ||
return; | ||
} | ||
|
||
let scopesData; | ||
try { | ||
scopesData = await fetchScopeData(accountId, scopeGroup); | ||
} catch (e) { | ||
logger.debug(`Error verifying access of scopeGroup ${scopeGroup}: ${e}`); | ||
return; | ||
} | ||
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; | ||
} | ||
} | ||
|
||
/** | ||
* Logs a message for an error instance resulting from API interaction | ||
* related to serverless function. | ||
* | ||
* @param {int} accountId | ||
* @param {Error|SystemError|Object} error | ||
* @param {ApiErrorContext} context | ||
*/ | ||
async function logServerlessFunctionApiErrorInstance( | ||
accountId, | ||
error, | ||
context | ||
) { | ||
if (isMissingScopeError(error)) { | ||
await verifyAccessKeyAndUserAccess(accountId, SCOPE_GROUPS.functions); | ||
return; | ||
} | ||
|
||
// StatusCodeError | ||
if (isApiStatusCodeError(error)) { | ||
logApiStatusCodeError(error, context); | ||
return; | ||
} | ||
logErrorInstance(error, context); | ||
} | ||
|
||
module.exports = { | ||
ApiErrorContext, | ||
parseValidationErrors, | ||
logApiErrorInstance, | ||
logApiUploadErrorInstance, | ||
logServerlessFunctionApiErrorInstance, | ||
isMissingScopeError, | ||
isSpecifiedError, | ||
isSpecifiedHubSpotAuthError, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
const { logger } = require('@hubspot/cli-lib/logger'); | ||
const { | ||
ErrorContext, | ||
isSystemError, | ||
debugErrorAndContext, | ||
} = require('./standardErrors'); | ||
|
||
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 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); | ||
} | ||
|
||
module.exports = { | ||
FileSystemErrorContext, | ||
logFileSystemErrorInstance, | ||
}; |
Oops, something went wrong.