Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature (WIP): Add support for default account override #1349

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
26 changes: 25 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const {
getConfigPath,
validateConfig,
} = require('@hubspot/local-dev-lib/config');
const {
DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID,
DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND,
} = require('@hubspot/local-dev-lib/constants/config');
const { logError } = require('../lib/errorHandlers/index');
const { setLogLevel, getCommandName } = require('../lib/commonOpts');
const { validateAccount } = require('../lib/validation');
Expand Down Expand Up @@ -202,7 +206,27 @@ const injectAccountIdMiddleware = async options => {
if (options.useEnv && process.env.HUBSPOT_ACCOUNT_ID) {
options.derivedAccountId = parseInt(process.env.HUBSPOT_ACCOUNT_ID, 10);
} else {
options.derivedAccountId = getAccountId(account);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm realizing the implication of getAccountId potentially throwing an error. Before, the worst case scenario was that getAccountId could return null, but now it can throw an error. Does that mean we have to go through every usage of it in the app and wrap them all in try/catch blocks? I wonder if there's some way to get around having to do that 🤔

Is it safe to just make the assumption that any potential thrown errors from getAccountId() would get caught here in the middleware, before any of the code in the commands can execute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we can safely assume that any potential errors from getAccountId() will be caught in injectAccountIdMiddleware.

Apart from the init and auth commands, injectAccountMiddleware will run in the middleware before every command handler. Actually, the work you did to eliminate multiple getAccountId functions guarantees that--now we call the same function every time, so we know that if it fails in the command, it will also fail in middleware.

try {
options.derivedAccountId = getAccountId(account);
} catch (error) {
logError(error);
if (error.cause === DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID) {
logger.log(
i18n(`${i18nKey}.injectAccountIdMiddleware.invalidAccountId`, {
overrideCommand: uiCommandReference('hs account create-override'),
})
);
}
if (error.cause === DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND) {
logger.log(
i18n(`${i18nKey}.injectAccountIdMiddleware.accountNotFound`, {
configPath: getConfigPath(),
authCommand: uiCommandReference('hs account auth'),
})
);
}
process.exit(EXIT_CODES.ERROR);
}
}
};

Expand Down
3 changes: 3 additions & 0 deletions commands/__tests__/accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import use from '../account/use';
import * as info from '../account/info';
import remove from '../account/remove';
import clean from '../account/clean';
import * as createOverride from '../account/createOverride';

jest.mock('yargs');
jest.mock('../account/list');
Expand All @@ -14,6 +15,7 @@ jest.mock('../account/use');
jest.mock('../account/info');
jest.mock('../account/remove');
jest.mock('../account/clean');
jest.mock('../account/createOverride');
jest.mock('../../lib/commonOpts');
yargs.command.mockReturnValue(yargs);
yargs.demandCommand.mockReturnValue(yargs);
Expand Down Expand Up @@ -42,6 +44,7 @@ describe('commands/account', () => {
['info', info],
['remove', remove],
['clean', clean],
['createOverride', createOverride],
];

it('should demand the command takes one positional argument', () => {
Expand Down
2 changes: 2 additions & 0 deletions commands/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const use = require('./account/use');
const info = require('./account/info');
const remove = require('./account/remove');
const clean = require('./account/clean');
const createOverride = require('./account/createOverride');

const i18nKey = 'commands.account';

Expand All @@ -23,6 +24,7 @@ exports.builder = yargs => {
.command(info)
.command(remove)
.command(clean)
.command(createOverride)
.demandCommand(1, '');

return yargs;
Expand Down
77 changes: 77 additions & 0 deletions commands/account/createOverride.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from 'fs-extra';
import path from 'path';
import { Argv, ArgumentsCamelCase } from 'yargs';
import { getCwd } from '@hubspot/local-dev-lib/path';
import { logger } from '@hubspot/local-dev-lib/logger';
import { DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME } from '@hubspot/local-dev-lib/constants/config';
import { getConfigPath, getAccountId } from '@hubspot/local-dev-lib/config';
import { addConfigOptions } from '../../lib/commonOpts';
import { i18n } from '../../lib/lang';
import { EXIT_CODES } from '../../lib/enums/exitCodes';
import { selectAccountFromConfig } from '../../lib/prompts/accountsPrompt';
import { logError } from '../../lib/errorHandlers/index';
import { CommonArgs, ConfigArgs } from '../../types/Yargs';

const i18nKey = 'commands.account.subcommands.createOverride';

export const describe = null; // i18n(`${i18nKey}.describe`);

export const command = 'create-override [account]';

type AccountInfoArgs = CommonArgs &
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like a copy/paste leftover here 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not quite sure what you mean--I based the type off of the types in the hs account info command but had to make some changes. Should I move this to Types, so we can reuse between commands?

ConfigArgs & {
account: string | number;
};

export async function handler(
args: ArgumentsCamelCase<AccountInfoArgs>
): Promise<void> {
let overrideDefaultAccount = args.account;

if (!overrideDefaultAccount) {
overrideDefaultAccount = await selectAccountFromConfig();
} else if (!getAccountId(overrideDefaultAccount)) {
logger.error(
i18n(`${i18nKey}.errors.accountNotFound`, {
configPath: getConfigPath() || '',
})
);
overrideDefaultAccount = await selectAccountFromConfig();
}
const accountId = getAccountId(overrideDefaultAccount);

try {
const overrideFilePath = path.join(
getCwd(),
DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME
);
await fs.writeFile(overrideFilePath, accountId!.toString(), 'utf8');
logger.success(i18n(`${i18nKey}.success`, { overrideFilePath }));
kemmerle marked this conversation as resolved.
Show resolved Hide resolved
process.exit(EXIT_CODES.SUCCESS);
} catch (e: unknown) {
kemmerle marked this conversation as resolved.
Show resolved Hide resolved
logError(e);
process.exit(EXIT_CODES.ERROR);
}
}

export function builder(yargs: Argv): Argv<AccountInfoArgs> {
kemmerle marked this conversation as resolved.
Show resolved Hide resolved
addConfigOptions(yargs);

yargs.positional('account', {
describe: i18n(`${i18nKey}.options.account.describe`),
type: 'string',
});
yargs.example([
['$0 account create-override', i18n(`${i18nKey}.examples.default`)],
[
'$0 account create-override 12345678',
i18n(`${i18nKey}.examples.idBased`),
],
[
'$0 account create-override MyAccount',
i18n(`${i18nKey}.examples.nameBased`),
],
]);

return yargs as Argv<AccountInfoArgs>;
}
5 changes: 5 additions & 0 deletions commands/account/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
getConfigPath,
getConfigDefaultAccount,
getConfigAccounts,
getDefaultAccountOverrideFilePath,
} = require('@hubspot/local-dev-lib/config');
const {
getAccountIdentifier,
Expand Down Expand Up @@ -85,6 +86,7 @@ exports.handler = async options => {
trackCommandUsage('accounts-list', null, derivedAccountId);

const configPath = getConfigPath();
const overrideFilePath = getDefaultAccountOverrideFilePath();
const accountsList = getConfigAccounts();
const mappedPortalData = sortAndMapPortals(accountsList);
const portalData = getPortalData(mappedPortalData);
Expand All @@ -97,6 +99,9 @@ exports.handler = async options => {
);

logger.log(i18n(`${i18nKey}.configPath`, { configPath }));
if (overrideFilePath) {
logger.log(i18n(`${i18nKey}.overrideFilePath`, { overrideFilePath }));
}
logger.log(
i18n(`${i18nKey}.defaultAccount`, {
account: getConfigDefaultAccount(),
Expand Down
21 changes: 21 additions & 0 deletions lang/en.lyaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,34 @@ en:
portalEnvVarDeprecated: "The HUBSPOT_PORTAL_ID environment variable is deprecated. Please use HUBSPOT_ACCOUNT_ID instead."
loadConfigMiddleware:
configFileExists: "A configuration file already exists at {{ configPath }}. To specify a new configuration file, delete the existing one and try again."
injectAccountIdMiddleware:
invalidAccountId: "In the default override file (.hsaccount), the account ID must be a number. Please delete the current file and generate a new one using {{ overrideCommand }}."
accountNotFound: "The account in the default override file (.hsaccount) wasn't found in your configured accounts. You can authorize this account using {{ authCommand }}."
completion:
describe: "Enable bash completion shortcuts for commands. Concat the generated script to your .bashrc, .bash_profile, or .zshrc file."
examples:
default: "Generate shell completion scripts for the zsh shell"
account:
describe: "Commands for managing configured accounts."
subcommands:
createOverride:
describe: "Create a new default account override file (.hs-account) in the current working directory."
success: "Default account override file created at {{ overrideFilePath }}"
errors:
accountNotFound: "The specified account could not be found in the config file {{ configPath }}"
options:
account:
describe: "Name or ID of the account to create an override file for."
examples:
default: "Create a new default account override file (.hs-account) in the current working directory"
idBased: "Create a new default account override file (.hs-account) in the current working directory, using the account with accountId \"1234567\""
nameBased: "Create a new default account override file (.hs-account) in the current working directory, using the account with name \"MyAccount\""
list:
accounts: "{{#bold}}Accounts{{/bold}}:"
defaultAccount: "{{#bold}}Default account{{/bold}}: {{ account }}"
describe: "List names of accounts defined in config."
configPath: "{{#bold}}Config path{{/bold}}: {{ configPath }}"
overrideFilePath: "{{#bold}}Default account override file path{{/bold}}: {{ overrideFilePath }}"
labels:
accountId: "Account ID"
authType: "Auth Type"
Expand Down Expand Up @@ -1511,6 +1527,9 @@ en:
doctor:
runningDiagnostics: "Running diagnostics..."
diagnosticsComplete: "Diagnostics complete"
defaultAccountOverrideFileChecks:
overrideActive: "Default account override file active: {{ defaultAccountOverrideFile }}"
overrideAccountId: "Active account ID: {{ overrideAccountId }}"
accountChecks:
active: "Default account active"
inactive: "Default account isn't active"
Expand Down Expand Up @@ -1557,6 +1576,8 @@ en:
defaultAccountSubHeader: "Default Account: {{accountDetails}}"
noConfigFile: "CLI configuration not found"
noConfigFileSecondary: "Run {{command}} and follow the prompts to create your CLI configuration file and connect it to your HubSpot account"
defaultAccountOverrideFile:
header: "Default account override file path:"
projectConfig:
header: "Project configuration"
projectDirSubHeader: "Project dir: {{#bold}}{{ projectDir }}{{/bold}}"
Expand Down
9 changes: 9 additions & 0 deletions lib/doctor/Diagnosis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface DiagnosisCategories {
cli: DiagnosisCategory;
project: DiagnosisCategory;
cliConfig: DiagnosisCategory;
defaultAccountOverrideFile: DiagnosisCategory;
}

const i18nKey = `lib.doctor.diagnosis`;
Expand Down Expand Up @@ -59,6 +60,10 @@ export class Diagnosis {
header: i18n(`${i18nKey}.cliConfig.header`),
sections: [],
},
defaultAccountOverrideFile: {
header: i18n(`${i18nKey}.defaultAccountOverrideFile.header`),
sections: [],
},
project: {
header: i18n(`${i18nKey}.projectConfig.header`),
subheaders: [
Expand Down Expand Up @@ -109,6 +114,10 @@ export class Diagnosis {
this.diagnosis.cliConfig.sections.push(section);
}

addDefaultAccountOverrideFileSection(section: Section): void {
this.diagnosis.defaultAccountOverrideFile.sections.push(section);
}

toString(): string {
const output = [];
for (const value of Object.values(this.diagnosis)) {
Expand Down
7 changes: 6 additions & 1 deletion lib/doctor/DiagnosticInfoBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
AuthType,
} from '@hubspot/local-dev-lib/types/Accounts';
import { Project } from '@hubspot/local-dev-lib/types/Project';
import { getAccountId } from '@hubspot/local-dev-lib/config';
import {
getAccountId,
getDefaultAccountOverrideFilePath,
} from '@hubspot/local-dev-lib/config';
import { getAccountConfig, getConfigPath } from '@hubspot/local-dev-lib/config';
import { getAccessToken } from '@hubspot/local-dev-lib/personalAccessKey';
import { walk } from '@hubspot/local-dev-lib/fs';
Expand All @@ -36,6 +39,7 @@ export interface DiagnosticInfo extends FilesInfo {
path?: string;
versions: { [hubspotCli]: string; node: string; npm: string | null };
config: string | null;
defaultAccountOverrideFile: string | null | undefined;
project: {
details?: Project;
config?: ProjectConfig;
Expand Down Expand Up @@ -110,6 +114,7 @@ export class DiagnosticInfoBuilder {
arch,
path: mainModule?.path,
config: getConfigPath(),
defaultAccountOverrideFile: getDefaultAccountOverrideFilePath(),
versions: {
[hubspotCli]: pkg.version,
node,
Expand Down
26 changes: 25 additions & 1 deletion lib/doctor/Doctor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { logger } from '@hubspot/local-dev-lib/logger';
import { getAccountId } from '@hubspot/local-dev-lib/config';
import {
getAccountId,
getCWDAccountOverride,
} from '@hubspot/local-dev-lib/config';

import SpinniesManager from '../ui/SpinniesManager';
import {
Expand Down Expand Up @@ -64,6 +67,7 @@ export class Doctor {
await Promise.all([
...this.performCliChecks(),
...this.performCliConfigChecks(),
...this.performDefaultAccountOverrideFileChecks(),
...(this.projectConfig?.projectConfig ? this.performProjectChecks() : []),
]);

Expand Down Expand Up @@ -112,6 +116,26 @@ export class Doctor {
return [this.checkIfAccessTokenValid()];
}

private performDefaultAccountOverrideFileChecks(): Array<Promise<void>> {
const localI18nKey = `${i18nKey}.defaultAccountOverrideFileChecks`;
if (this.diagnosticInfo?.defaultAccountOverrideFile) {
this.diagnosis?.addDefaultAccountOverrideFileSection({
type: 'warning',
message: i18n(`${localI18nKey}.overrideActive`, {
defaultAccountOverrideFile:
this.diagnosticInfo.defaultAccountOverrideFile,
}),
});
this.diagnosis?.addDefaultAccountOverrideFileSection({
type: 'warning',
message: i18n(`${localI18nKey}.overrideAccountId`, {
overrideAccountId: getCWDAccountOverride(),
}),
});
}
return [];
}

private async checkIfAccessTokenValid(): Promise<void> {
const localI18nKey = `${i18nKey}.accountChecks`;
try {
Expand Down
1 change: 1 addition & 0 deletions lib/doctor/__tests__/Diagnosis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('lib/doctor/Diagnosis', () => {
account: {},
arch: process.arch,
config: 'path/to/config.json',
defaultAccountOverrideFile: 'path/to/default/account/override/.hsaccount',
configFiles: [],
envFiles: [],
files: [],
Expand Down
10 changes: 10 additions & 0 deletions lib/doctor/__tests__/DiagnosticInfoBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getAccountId as _getAccountId,
getAccountConfig as _getAccountConfig,
getConfigPath as _getConfigPath,
getDefaultAccountOverrideFilePath as _getDefaultAccountOverrideFilePath,
} from '@hubspot/local-dev-lib/config';
import { getAccessToken as _getAccessToken } from '@hubspot/local-dev-lib/personalAccessKey';
import { walk as _walk } from '@hubspot/local-dev-lib/fs';
Expand All @@ -38,6 +39,10 @@ const getAccountConfig = _getAccountConfig as jest.MockedFunction<
const getConfigPath = _getConfigPath as jest.MockedFunction<
typeof _getConfigPath
>;
const getDefaultAccountOverrideFilePath =
_getDefaultAccountOverrideFilePath as jest.MockedFunction<
typeof _getDefaultAccountOverrideFilePath
>;
const getAccountId = _getAccountId as jest.MockedFunction<typeof _getAccountId>;
const getProjectConfig = _getProjectConfig as jest.MockedFunction<
typeof _getProjectConfig
Expand Down Expand Up @@ -116,6 +121,8 @@ describe('lib/doctor/DiagnosticInfo', () => {

const npmVersion = 'v8.17.0';
const configPath = '/path/to/config';
const defaultAccountOverrideFile =
'path/to/default/account/override/.hsaccount';

beforeEach(() => {
builder = new DiagnosticInfoBuilder(processInfo);
Expand Down Expand Up @@ -157,6 +164,9 @@ describe('lib/doctor/DiagnosticInfo', () => {
} as unknown as HubSpotPromise<Project>);
getAccessToken.mockResolvedValue(accessToken);
getConfigPath.mockReturnValue(configPath);
getDefaultAccountOverrideFilePath.mockReturnValue(
defaultAccountOverrideFile
);
utilPromisify.mockReturnValue(jest.fn().mockResolvedValue(npmVersion));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exports[`lib/doctor/DiagnosticInfo generateDiagnosticInfo should gather the requ
"src/app/public-app.json",
"src/app/app.functions/serverless.json",
],
"defaultAccountOverrideFile": "path/to/default/account/override/.hsaccount",
"envFiles": [
"src/app/app.functions/.env",
],
Expand Down
Loading