Skip to content

Commit

Permalink
feat(cli): add support to associate a search hub when creating a sear…
Browse files Browse the repository at this point in the history
  • Loading branch information
olamothe committed Jan 31, 2024
1 parent dbb43e8 commit fb931e2
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 14 deletions.
12 changes: 11 additions & 1 deletion packages/cli/commons/src/platform/authenticatedClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,22 @@ describe('AuthenticatedClient', () => {
fancyIt()(
'#createImpersonateApiKey should create an API key with impersonate privileges',
async () => {
await new AuthenticatedClient().createImpersonateApiKey('my-key');
await new AuthenticatedClient().createImpersonateApiKey(
'my-key',
'my-hub'
);

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
displayName: 'cli-my-key',
description: 'Generated by the Coveo CLI',
additionalConfiguration: {
search: {
enforcedQueryPipelineConfiguration: {
searchHub: 'my-hub',
},
},
},
enabled: true,
privileges: [
{owner: 'SEARCH_API', targetDomain: 'IMPERSONATE', targetId: '*'},
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/commons/src/platform/authenticatedClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,20 @@ export class AuthenticatedClient {
return orgs.some((o) => o.id === org);
}

public async createImpersonateApiKey(name: string) {
public async createImpersonateApiKey(name: string, searchHub?: string) {
const platformClient = await this.getClient();
return await platformClient.apiKey.create({
displayName: `cli-${name}`,
description: 'Generated by the Coveo CLI',
...(searchHub && {
additionalConfiguration: {
search: {
enforcedQueryPipelineConfiguration: {
searchHub,
},
},
},
}),
enabled: true,
privileges: [
{targetDomain: 'IMPERSONATE', targetId: '*', owner: 'SEARCH_API'},
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/commons/src/preconditions/platformPrivilege.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,18 @@ export const readOrganizationPrivilege: PlatformPrivilege = {
: `You are not authorized to view the organization. Make sure you are granted this privilege before running the command again.
See https://docs.coveo.com/en/1707#organization-domain`,
};

export const listSearchHubsPrivilege: PlatformPrivilege = {
models: [
{
type: 'VIEW',
targetDomain: 'SEARCH_USAGE_METRICS',
targetId: '*',
owner: 'SEARCH_API',
},
],
unsatisfiedConditionMessage: (anonymous: boolean) =>
anonymous
? 'Your access token is missing the privilege to list available search hubs in your organization. Make sure to grant this privilege before running the command again. See https://docs.coveo.com/en/1707/#search-usage-metrics-domain.'
: 'You are not authorized to list available search hubs in your organization. Please contact an administrator of your Coveo organization and ask for that privilege. See https://docs.coveo.com/en/1707/#search-usage-metrics-domain.',
};
24 changes: 24 additions & 0 deletions packages/cli/commons/src/utils/ux.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {CliUx} from '@oclif/core';
import isCi from 'is-ci';
import {red, green, magenta} from 'chalk';
import inquirer from 'inquirer';

function isWindows() {
return process.platform === 'win32';
Expand Down Expand Up @@ -36,3 +37,26 @@ export const formatOrgId = (orgId: TemplateStringsArray | string) =>

export const confirm = (message: string, ciDefault: boolean) =>
isCi ? Promise.resolve(ciDefault) : CliUx.ux.confirm(message);

export const prompt = (message: string, ciDefault: string) =>
isCi ? Promise.resolve(ciDefault) : CliUx.ux.prompt(message);

export const promptChoices = async (
message: string,
choices: (string | inquirer.Separator)[],
ciDefault: string
) => {
if (isCi) {
return ciDefault;
}
const response = await inquirer.prompt([
{
name: 'choice',
message,
type: 'list',
choices,
},
]);

return response.choice;
};
7 changes: 6 additions & 1 deletion packages/cli/core/src/commands/atomic/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
createAtomicApp,
createAtomicLib,
} from '../../lib/atomic/createAtomicProject';
import {promptForSearchHub} from '../ui/create/shared';
import {AuthenticatedClient} from '@coveo/cli-commons/platform/authenticatedClient';

export default class AtomicInit extends CLICommand {
public static description =
Expand Down Expand Up @@ -67,11 +69,14 @@ export default class AtomicInit extends CLICommand {
}

@Before(...atomicAppPreconditions)
private createAtomicApp(projectName: string) {
private async createAtomicApp(projectName: string) {
const cfg = this.configuration.get();
const client = await new AuthenticatedClient().getClient();
const searchHub = await promptForSearchHub(client);
return createAtomicApp({
projectName,
cfg,
searchHub,
});
}

Expand Down
20 changes: 17 additions & 3 deletions packages/cli/core/src/commands/ui/create/angular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import {IsNgVersionInRange} from '../../../lib/decorators/preconditions/ng';
import {
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege,
} from '@coveo/cli-commons/preconditions/platformPrivilege';
import {Trackable} from '@coveo/cli-commons/preconditions/trackable';
import {promptForSearchHub} from './shared';

export default class Angular extends CLICommand {
public static templateName = '@coveo/angular';
Expand Down Expand Up @@ -66,7 +68,11 @@ export default class Angular extends CLICommand {
IsNodeVersionInRange(Angular.requiredNodeVersion),
IsNpmVersionInRange(Angular.requiredNpmVersion),
IsNgVersionInRange(Angular.requiredNgVersion),
HasNecessaryCoveoPrivileges(createApiKeyPrivilege, impersonatePrivilege)
HasNecessaryCoveoPrivileges(
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege
)
)
public async run() {
const {args, flags} = await this.parse(Angular);
Expand All @@ -86,10 +92,18 @@ export default class Angular extends CLICommand {
}

private async addCoveoToProject(applicationName: string, defaults: boolean) {
const authenticatedClient = new AuthenticatedClient();

const platformClient = await authenticatedClient.getClient();
const searchHub = await promptForSearchHub(platformClient);

const {flags, args} = await this.parse(Angular);
const cfg = this.configuration.get();
const authenticatedClient = new AuthenticatedClient();
const apiKey = await authenticatedClient.createImpersonateApiKey(args.name);

const apiKey = await authenticatedClient.createImpersonateApiKey(
args.name,
searchHub
);
const username = await authenticatedClient.getUsername();
const schematicVersion =
flags.version || getPackageVersion(Angular.templateName);
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/core/src/commands/ui/create/atomic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
atomicAppPreconditions,
createAtomicApp,
} from '../../../lib/atomic/createAtomicProject';
import {AuthenticatedClient} from '@coveo/cli-commons/platform/authenticatedClient';
import {promptForSearchHub} from './shared';

export default class Atomic extends CLICommand {
public static description =
Expand Down Expand Up @@ -43,12 +45,15 @@ export default class Atomic extends CLICommand {
public async run() {
const {flags, args} = await this.parse(Atomic);
const cfg = this.configuration.get();
const client = await new AuthenticatedClient().getClient();
const searchHub = await promptForSearchHub(client);

await createAtomicApp({
initializerVersion: flags.version,
pageId: flags.pageId,
projectName: args.name,
cfg,
searchHub,
});
}

Expand Down
21 changes: 17 additions & 4 deletions packages/cli/core/src/commands/ui/create/react.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CLICommand} from '@coveo/cli-commons/command/cliCommand';
import {Flags} from '@oclif/core';
import {CliUx, Flags} from '@oclif/core';
import {Config} from '@coveo/cli-commons/config/config';
import {platformUrl} from '@coveo/cli-commons/platform/environment';
import {AuthenticatedClient} from '@coveo/cli-commons/platform/authenticatedClient';
Expand All @@ -15,12 +15,14 @@ import {
import {
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege,
} from '@coveo/cli-commons/preconditions/platformPrivilege';
import {Trackable} from '@coveo/cli-commons/preconditions/trackable';
import {
IsNodeVersionInRange,
IsNpxInstalled,
} from '../../../lib/decorators/preconditions';
import {promptForSearchHub} from './shared';

type ReactProcessEnv = {
orgId: string;
Expand Down Expand Up @@ -72,7 +74,11 @@ export default class React extends CLICommand {
IsAuthenticated([AuthenticationType.OAuth]),
IsNodeVersionInRange(React.requiredNodeVersion),
IsNpxInstalled(),
HasNecessaryCoveoPrivileges(createApiKeyPrivilege, impersonatePrivilege)
HasNecessaryCoveoPrivileges(
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege
)
)
public async run() {
const {args} = await this.parse(React);
Expand All @@ -81,11 +87,18 @@ export default class React extends CLICommand {
}

private async setupEnvironmentVariables(name: string) {
const authenticatedClient = new AuthenticatedClient();
const platformClient = await authenticatedClient.getClient();
const searchHub = await promptForSearchHub(platformClient);

const {args} = await this.parse(React);
const cfg = this.configuration.get();
const authenticatedClient = new AuthenticatedClient();

const username = await authenticatedClient.getUsername();
const apiKey = await authenticatedClient.createImpersonateApiKey(args.name);
const apiKey = await authenticatedClient.createImpersonateApiKey(
args.name,
searchHub
);

const env: ReactProcessEnv = {
orgId: cfg.organization,
Expand Down
37 changes: 37 additions & 0 deletions packages/cli/core/src/commands/ui/create/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {confirm, promptChoices, prompt} from '@coveo/cli-commons/utils/ux';
import PlatformClient from '@coveo/platform-client';
import inquirer from 'inquirer';

const manuallyEnterSearchHub = () =>
prompt('Enter the search hub you want to use', '');

export async function promptForSearchHub(client: PlatformClient) {
const createSearchHub = await confirm(
'An API key will be created against your organization. We strongly recommends that you associate this API key with a search hub. Would you like to do so now ? y/n',
false
);
if (!createSearchHub) {
return;
}

const availableHubs = await client.searchUsageMetrics.searchHubs.list();
if (availableHubs.hubs.length === 0) {
return await manuallyEnterSearchHub();
}

const choice = await promptChoices(
'Use an existing search hub, or create a new one?',
[
new inquirer.Separator(),
'Create a new one',
new inquirer.Separator(),
...availableHubs.hubs.map((hub) => hub.name),
],
''
);

if (choice === 'Create a new one') {
return await manuallyEnterSearchHub();
}
return choice;
}
18 changes: 15 additions & 3 deletions packages/cli/core/src/commands/ui/create/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {appendCmdIfWindows} from '@coveo/cli-commons/utils/os';
import {
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege,
} from '@coveo/cli-commons/preconditions/platformPrivilege';
import {Trackable} from '@coveo/cli-commons/preconditions/trackable';
import {
Expand All @@ -25,6 +26,7 @@ import {
import {cwd} from 'node:process';
import {mkdirSync, readdirSync, statSync, writeFileSync} from 'node:fs';
import dedent from 'ts-dedent';
import {promptForSearchHub} from './shared';

export default class Vue extends CLICommand {
public static packageName = '@coveo/create-headless-vue';
Expand Down Expand Up @@ -67,7 +69,11 @@ export default class Vue extends CLICommand {
IsAuthenticated([AuthenticationType.OAuth]),
IsNodeVersionInRange(Vue.requiredNodeVersion),
IsNpxInstalled(),
HasNecessaryCoveoPrivileges(createApiKeyPrivilege, impersonatePrivilege)
HasNecessaryCoveoPrivileges(
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege
)
)
public async run() {
const {args, flags} = await this.parse(Vue);
Expand All @@ -82,11 +88,17 @@ export default class Vue extends CLICommand {
}

private async createEnvFile(dirName: string) {
const authenticatedClient = new AuthenticatedClient();
const platformClient = await authenticatedClient.getClient();
const searchHub = await promptForSearchHub(platformClient);
const {args} = await this.parse(Vue);
const cfg = this.configuration.get();
const authenticatedClient = new AuthenticatedClient();

const username = await authenticatedClient.getUsername();
const apiKey = await authenticatedClient.createImpersonateApiKey(args.name);
const apiKey = await authenticatedClient.createImpersonateApiKey(
args.name,
searchHub
);

writeFileSync(
join(dirName, '.env'),
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/core/src/lib/atomic/createAtomicProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {
createApiKeyPrivilege,
impersonatePrivilege,
listSearchHubsPrivilege,
viewSearchPagesPrivilege,
} from '@coveo/cli-commons/preconditions/platformPrivilege';
import {
Expand All @@ -29,6 +30,7 @@ interface CreateAppOptions {
pageId?: string;
projectName: string;
cfg: Configuration;
searchHub: string;
}
export const atomicAppInitializerPackage = '@coveo/create-atomic';
export const atomicLibInitializerPackage =
Expand All @@ -50,7 +52,8 @@ export const atomicAppPreconditions = [
HasNecessaryCoveoPrivileges(
createApiKeyPrivilege,
impersonatePrivilege,
viewSearchPagesPrivilege
viewSearchPagesPrivilege,
listSearchHubsPrivilege
),
];

Expand Down Expand Up @@ -78,6 +81,8 @@ export async function createAtomicApp(options: CreateAppOptions) {
options.cfg.environment,
'--user',
username,
'--search-hub',
options.searchHub,
];

if (options.pageId) {
Expand Down
Loading

0 comments on commit fb931e2

Please sign in to comment.