From 9b39c4bdf04e76c9096beb3bf67aea0567187ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morel=20Se=CC=81bastien?= Date: Tue, 14 Jan 2025 13:06:33 -0800 Subject: [PATCH] feat(cli): enable staging for command with env variable --- package.json | 2 +- src/command/install-boilerplate.tsx | 9 +++++--- src/command/run-mass-operation.tsx | 23 +++++++++++-------- src/core/create-credentials-retriever.ts | 12 +++++++--- src/core/create-crystallize-client-builder.ts | 15 ++++++++++++ src/core/di.ts | 14 +++++++---- .../actions/download-project.tsx | 9 ++++---- .../actions/execute-recipes.tsx | 15 +++++++----- .../install-boilerplate.journey.tsx | 11 ++++++--- src/domain/use-cases/create-clean-tenant.ts | 10 ++++---- src/domain/use-cases/fetch-tips.ts | 1 + src/domain/use-cases/run-mass-operation.ts | 13 +++++++++-- src/index.ts | 11 ++++++--- 13 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 src/core/create-crystallize-client-builder.ts diff --git a/package.json b/package.json index a416907..d600fd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@crystallize/cli", - "version": "5.0.0", + "version": "5.0.1", "description": "Crystallize CLI", "module": "src/index.ts", "repository": "https://github.com/CrystallizeAPI/crystallize-cli", diff --git a/src/command/install-boilerplate.tsx b/src/command/install-boilerplate.tsx index 3bda9bb..e584f0e 100644 --- a/src/command/install-boilerplate.tsx +++ b/src/command/install-boilerplate.tsx @@ -8,7 +8,7 @@ import type { InstallBoilerplateStore } from '../core/journeys/install-boilerpla import { Provider } from 'jotai'; import type { CredentialRetriever } from '../domain/contracts/credential-retriever'; import type { Logger } from '../domain/contracts/logger'; -import type { QueryBus } from '../domain/contracts/bus'; +import type { QueryBus, CommandBus } from '../domain/contracts/bus'; type Deps = { logLevels: ('info' | 'debug')[]; @@ -17,6 +17,7 @@ type Deps = { credentialsRetriever: CredentialRetriever; logger: Logger; queryBus: QueryBus; + commandBus: CommandBus; }; export const createInstallBoilerplateCommand = ({ @@ -25,6 +26,7 @@ export const createInstallBoilerplateCommand = ({ installBoilerplateCommandStore, credentialsRetriever, queryBus, + commandBus, logLevels, }: Deps): Command => { const command = new Command('install-boilerplate'); @@ -34,8 +36,7 @@ export const createInstallBoilerplateCommand = ({ command.addArgument(new Argument('[boilerplate-identifier]', 'The boilerplate identifier to use.')); command.addOption(new Option('-b, --bootstrap-tenant', 'Bootstrap the tenant with initial data.')); - command.action(async (...args) => { - const [folder, tenantIdentifier, boilerplateIdentifier, flags] = args; + command.action(async (folder: string, tenantIdentifier: string, boilerplateIdentifier: string, flags) => { logger.setBuffered(true); await flySystem.createDirectoryOrFail( folder, @@ -67,6 +68,8 @@ export const createInstallBoilerplateCommand = ({ diff --git a/src/command/run-mass-operation.tsx b/src/command/run-mass-operation.tsx index 4c7ff9e..ba2d36b 100644 --- a/src/command/run-mass-operation.tsx +++ b/src/command/run-mass-operation.tsx @@ -3,17 +3,23 @@ import type { Logger } from '../domain/contracts/logger'; import type { CommandBus } from '../domain/contracts/bus'; import type { Operation, Operations } from '@crystallize/schema/mass-operation'; import type { CredentialRetriever } from '../domain/contracts/credential-retriever'; -import { createClient } from '@crystallize/js-api-client'; import pc from 'picocolors'; import { ZodError } from 'zod'; +import type { createClient } from '@crystallize/js-api-client'; type Deps = { logger: Logger; commandBus: CommandBus; credentialsRetriever: CredentialRetriever; + createCrystallizeClient: typeof createClient; }; -export const createRunMassOperationCommand = ({ logger, commandBus, credentialsRetriever }: Deps): Command => { +export const createRunMassOperationCommand = ({ + logger, + commandBus, + credentialsRetriever, + createCrystallizeClient, +}: Deps): Command => { const command = new Command('run-mass-operation'); command.description('Upload and start an Mass Operation Task in your tenant.'); command.addArgument(new Argument('', 'The tenant identifier to use.')); @@ -22,8 +28,7 @@ export const createRunMassOperationCommand = ({ logger, commandBus, credentialsR command.option('--token_secret ', 'Your access token secret.'); command.option('--legacy-spec', 'Use legacy spec format.'); - command.action(async (...args) => { - const [tenantIdentifier, file, flags] = args; + command.action(async (tenantIdentifier: string, file: string, flags) => { let operationsContent: Operations; if (flags.legacySpec) { logger.warn(`Using legacy spec... Converting to operations file...`); @@ -76,17 +81,17 @@ export const createRunMassOperationCommand = ({ logger, commandBus, credentialsR throw new Error('Task not started. Please check the logs for more information.'); } - const crystallizeClient = createClient({ + const crystallizeClient = createCrystallizeClient({ tenantIdentifier, accessTokenId: credentials.ACCESS_TOKEN_ID, accessTokenSecret: credentials.ACCESS_TOKEN_SECRET, }); - logger.info(`Now, Waiting for task to complete...`); - while (startedTask.status !== 'complete') { - logger.info(`Task status: ${pc.yellow(startedTask.status)}`); + logger.info(`Now, Waiting for task ${pc.yellow(startedTask.id)} to complete...`); + while (startedTask?.status !== 'complete') { + logger.info(`Task status: ${pc.yellow(startedTask?.status)}`); await new Promise((resolve) => setTimeout(resolve, 1000)); - const get = await crystallizeClient.nextPimApi(getMassOperationBulkTask, { id: startedTask.id }); + const get = await crystallizeClient.nextPimApi(getMassOperationBulkTask, { id: startedTask?.id }); if (get.bulkTask.error) { throw new Error(get.data.bulkTask.error); } diff --git a/src/core/create-credentials-retriever.ts b/src/core/create-credentials-retriever.ts index 8472283..a4fec09 100644 --- a/src/core/create-credentials-retriever.ts +++ b/src/core/create-credentials-retriever.ts @@ -8,8 +8,14 @@ type Deps = { options?: CredentialRetrieverOptions; fallbackFile: string; flySystem: FlySystem; + createCrystallizeClient: typeof createClient; }; -export const createCredentialsRetriever = ({ options, fallbackFile, flySystem }: Deps): CredentialRetriever => { +export const createCredentialsRetriever = ({ + options, + fallbackFile, + flySystem, + createCrystallizeClient, +}: Deps): CredentialRetriever => { const getCredentials = async (rOptions?: CredentialRetrieverOptions) => { if (rOptions?.token_id && rOptions?.token_secret) { return { @@ -52,7 +58,7 @@ export const createCredentialsRetriever = ({ options, fallbackFile, flySystem }: }; const checkCredentials = async (credentials: PimCredentials) => { - const apiClient = createClient({ + const apiClient = createCrystallizeClient({ tenantIdentifier: '', accessTokenId: credentials.ACCESS_TOKEN_ID, accessTokenSecret: credentials.ACCESS_TOKEN_SECRET, @@ -73,7 +79,7 @@ export const createCredentialsRetriever = ({ options, fallbackFile, flySystem }: }; const fetchAvailableTenantIdentifier = async (credentials: PimCredentials, identifier: string) => { - const apiClient = createClient({ + const apiClient = createCrystallizeClient({ tenantIdentifier: '', accessTokenId: credentials.ACCESS_TOKEN_ID, accessTokenSecret: credentials.ACCESS_TOKEN_SECRET, diff --git a/src/core/create-crystallize-client-builder.ts b/src/core/create-crystallize-client-builder.ts new file mode 100644 index 0000000..552b8ab --- /dev/null +++ b/src/core/create-crystallize-client-builder.ts @@ -0,0 +1,15 @@ +import { createClient, type ClientConfiguration, type CreateClientOptions } from '@crystallize/js-api-client'; + +type Deps = { + crystallizeEnvironment: 'staging' | 'production'; +}; +export const createCrystallizeClientBuilder = + ({ crystallizeEnvironment }: Deps): typeof createClient => + (configuration: ClientConfiguration, options?: CreateClientOptions) => + createClient( + { + ...configuration, + origin: crystallizeEnvironment === 'staging' ? '-dev.crystallize.digital' : '.crystallize.com', + }, + options, + ); diff --git a/src/core/di.ts b/src/core/di.ts index 0c93232..f3b6740 100644 --- a/src/core/di.ts +++ b/src/core/di.ts @@ -21,19 +21,24 @@ import { createS3Uploader } from './create-s3-uploader'; import os from 'os'; import { createRunMassOperationHandler } from '../domain/use-cases/run-mass-operation'; import { createRunMassOperationCommand } from '../command/run-mass-operation'; +import type { createClient } from '@crystallize/js-api-client'; +import { createCrystallizeClientBuilder } from './create-crystallize-client-builder'; -const buildServices = () => { +export const buildServices = () => { const logLevels = ( `${Bun.env.LOG_LEVELS}` === 'no-output' ? [] : ['info', ...`${Bun.env.LOG_LEVELS}`.split(',')] ) as ('info' | 'debug')[]; + const crystallizeEnvironment = `${Bun.env.CRYSTALLIZE_ENVIRONMENT}` === 'staging' ? 'staging' : 'production'; const logger = createLogger('cli', logLevels); const container = createContainer<{ logLevels: ('info' | 'debug')[]; + crystallizeEnvironment: 'staging' | 'production'; logger: Logger; queryBus: QueryBus; commandBus: CommandBus; flySystem: FlySystem; credentialsRetriever: CredentialRetriever; + createCrystallizeClient: typeof createClient; runner: ReturnType; s3Uploader: ReturnType; // use cases @@ -55,17 +60,18 @@ const buildServices = () => { }); container.register({ logLevels: asValue(logLevels), + crystallizeEnvironment: asValue(crystallizeEnvironment), logger: asValue(logger), queryBus: asFunction(() => createQueryBus()).singleton(), commandBus: asFunction(() => createCommandBus()).singleton(), flySystem: asFunction(createFlySystem).singleton(), credentialsRetriever: asFunction(createCredentialsRetriever) .inject(() => ({ - fallbackFile: `${os.homedir()}/.crystallize/credentials.json`, + fallbackFile: `${os.homedir()}/.crystallize/credentials${crystallizeEnvironment !== 'production' ? '-' + crystallizeEnvironment : ''}.json`, options: undefined, })) .singleton(), - + createCrystallizeClient: asFunction(createCrystallizeClientBuilder).singleton(), runner: asFunction(createRunner).singleton(), s3Uploader: asFunction(createS3Uploader).singleton(), @@ -111,5 +117,3 @@ const buildServices = () => { ], }; }; -const services = buildServices(); -export const { logger, createCommand, createQuery, dispatchCommand, dispatchQuery, commands } = services; diff --git a/src/core/journeys/install-boilerplate/actions/download-project.tsx b/src/core/journeys/install-boilerplate/actions/download-project.tsx index dbd7ef6..66441fe 100644 --- a/src/core/journeys/install-boilerplate/actions/download-project.tsx +++ b/src/core/journeys/install-boilerplate/actions/download-project.tsx @@ -2,25 +2,26 @@ import { Box, Text } from 'ink'; import { useEffect } from 'react'; import { colors } from '../../../styles'; import { Spinner } from '../../../../ui/components/spinner'; -import { createQuery, dispatchQuery } from '../../../di'; import type { InstallBoilerplateStore } from '../create-store'; import { useAtom } from 'jotai'; +import type { QueryBus } from '../../../../domain/contracts/bus'; type DownloadProjectProps = { store: InstallBoilerplateStore['atoms']; + queryBus: QueryBus; }; -export const DownloadProject = ({ store }: DownloadProjectProps) => { +export const DownloadProject = ({ store, queryBus }: DownloadProjectProps) => { const [state] = useAtom(store.stateAtom); const [, boilerplateDownloaded] = useAtom(store.setDownloadedAtom); const [isWizardFullfilled] = useAtom(store.isWizardFullfilledAtom); useEffect(() => { if (state.boilerplate) { - const query = createQuery('DownloadBoilerplateArchive', { + const query = queryBus.createQuery('DownloadBoilerplateArchive', { boilerplate: state.boilerplate, destination: state.folder!, }); - dispatchQuery(query).then(() => boilerplateDownloaded(true)); + queryBus.dispatch(query).then(() => boilerplateDownloaded(true)); } }, []); diff --git a/src/core/journeys/install-boilerplate/actions/execute-recipes.tsx b/src/core/journeys/install-boilerplate/actions/execute-recipes.tsx index ec86f29..6f800de 100644 --- a/src/core/journeys/install-boilerplate/actions/execute-recipes.tsx +++ b/src/core/journeys/install-boilerplate/actions/execute-recipes.tsx @@ -2,9 +2,10 @@ import { Box, Text } from 'ink'; import { useEffect, useState } from 'react'; import { colors } from '../../../styles'; import { Spinner } from '../../../../ui/components/spinner'; -import { createCommand, dispatchCommand, logger } from '../../../di'; import type { InstallBoilerplateStore } from '../create-store'; import { useAtom } from 'jotai'; +import type { CommandBus } from '../../../../domain/contracts/bus'; +import type { Logger } from '../../../../domain/contracts/logger'; const feedbacks = [ 'Fetching the dependencies...', @@ -21,8 +22,10 @@ const feedbacks = [ type ExecuteRecipesProps = { store: InstallBoilerplateStore['atoms']; + commandBus: CommandBus; + logger: Logger; }; -export const ExecuteRecipes = ({ store }: ExecuteRecipesProps) => { +export const ExecuteRecipes = ({ store, commandBus, logger }: ExecuteRecipesProps) => { const [state] = useAtom(store.stateAtom); const [isWizardFullfilled] = useAtom(store.isWizardFullfilledAtom); const [, startImport] = useAtom(store.startBoostrappingAtom); @@ -36,20 +39,20 @@ export const ExecuteRecipes = ({ store }: ExecuteRecipesProps) => { } (async () => { - const setupBoilerplateCommand = createCommand('SetupBoilerplateProject', { + const setupBoilerplateCommand = commandBus.createCommand('SetupBoilerplateProject', { folder: state.folder!, credentials: state.credentials, tenant: state.tenant!, }); const [setupResult, tenantResult] = await Promise.allSettled([ - dispatchCommand(setupBoilerplateCommand), + commandBus.dispatch(setupBoilerplateCommand), (async () => { if (state.bootstrapTenant) { - const createTenantCommand = createCommand('CreateCleanTenant', { + const createTenantCommand = commandBus.createCommand('CreateCleanTenant', { tenant: state.tenant!, credentials: state.credentials!, }); - await dispatchCommand(createTenantCommand); + await commandBus.dispatch(createTenantCommand); startImport(); } })(), diff --git a/src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx b/src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx index 862a48c..0314575 100644 --- a/src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx +++ b/src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx @@ -13,17 +13,22 @@ import { Success } from '../../../ui/components/success'; import type { InstallBoilerplateStore } from './create-store'; import { useAtom } from 'jotai'; import type { CredentialRetriever } from '../../../domain/contracts/credential-retriever'; -import type { QueryBus } from '../../../domain/contracts/bus'; +import type { CommandBus, QueryBus } from '../../../domain/contracts/bus'; +import type { Logger } from '../../../domain/contracts/logger'; type InstallBoilerplateJourneyProps = { store: InstallBoilerplateStore['atoms']; credentialsRetriever: CredentialRetriever; queryBus: QueryBus; + logger: Logger; + commandBus: CommandBus; }; export const InstallBoilerplateJourney = ({ store, credentialsRetriever, queryBus, + commandBus, + logger, }: InstallBoilerplateJourneyProps) => { const [state] = useAtom(store.stateAtom); const [, changeTenant] = useAtom(store.changeTenantAtom); @@ -57,8 +62,8 @@ export const InstallBoilerplateJourney = ({ }} /> )} - {isWizardFullfilled && } - {state.isDownloaded && } + {isWizardFullfilled && } + {state.isDownloaded && } {!isWizardFullfilled && } diff --git a/src/domain/use-cases/create-clean-tenant.ts b/src/domain/use-cases/create-clean-tenant.ts index 77f4fc9..7fa3641 100644 --- a/src/domain/use-cases/create-clean-tenant.ts +++ b/src/domain/use-cases/create-clean-tenant.ts @@ -1,10 +1,12 @@ -import { createClient } from '@crystallize/js-api-client'; +import type { createClient } from '@crystallize/js-api-client'; import { jsonToGraphQLQuery } from 'json-to-graphql-query'; import type { Tenant } from '../contracts/models/tenant'; import type { CommandHandlerDefinition, Envelope } from 'missive.js'; import type { PimCredentials } from '../contracts/models/credentials'; -type Deps = {}; +type Deps = { + createCrystallizeClient: typeof createClient; +}; type Command = { tenant: Tenant; credentials: PimCredentials; @@ -18,13 +20,13 @@ export type CreateCleanTenantHandlerDefinition = CommandHandlerDefinition< const handler = async ( envelope: Envelope, - _: Deps, + { createCrystallizeClient }: Deps, ): Promise<{ id: string; identifier: string; }> => { const { tenant, credentials } = envelope.message; - const client = createClient({ + const client = createCrystallizeClient({ tenantIdentifier: '', accessTokenId: credentials.ACCESS_TOKEN_ID, accessTokenSecret: credentials.ACCESS_TOKEN_SECRET, diff --git a/src/domain/use-cases/fetch-tips.ts b/src/domain/use-cases/fetch-tips.ts index 904d4d6..083e7d4 100644 --- a/src/domain/use-cases/fetch-tips.ts +++ b/src/domain/use-cases/fetch-tips.ts @@ -16,6 +16,7 @@ export type FetchTipsHandlerDefinition = QueryHandlerDefinition< const handler = async (_: Envelope, deps: Deps) => { const { logger } = deps; + // we call production all the time here on purpose. const apiClient = createClient({ tenantIdentifier: 'crystallize_marketing', }); diff --git a/src/domain/use-cases/run-mass-operation.ts b/src/domain/use-cases/run-mass-operation.ts index a2ae00e..a89d981 100644 --- a/src/domain/use-cases/run-mass-operation.ts +++ b/src/domain/use-cases/run-mass-operation.ts @@ -9,6 +9,7 @@ import type { S3Uploader } from '../contracts/s3-uploader'; type Deps = { logger: Logger; s3Uploader: S3Uploader; + createCrystallizeClient: typeof createClient; }; type Command = { @@ -23,10 +24,18 @@ export type RunMassOperationHandlerDefinition = CommandHandlerDefinition< Awaited> >; -const handler = async (envelope: Envelope, { logger, s3Uploader }: Deps) => { +const handler = async ( + envelope: Envelope, + { logger, s3Uploader, createCrystallizeClient }: Deps, +): Promise<{ + task: { + id: string; + status: string; + }; +}> => { const { tenantIdentifier, operations: operationsContent } = envelope.message; - const crystallizeClient = createClient({ + const crystallizeClient = createCrystallizeClient({ tenantIdentifier, accessTokenId: envelope.message.credentials.ACCESS_TOKEN_ID, accessTokenSecret: envelope.message.credentials.ACCESS_TOKEN_SECRET, diff --git a/src/index.ts b/src/index.ts index 5661968..30005cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ #!/usr/bin/env bun -import { Command } from 'commander'; +import { Command, Option } from 'commander'; import packageJson from '../package.json'; import pc from 'picocolors'; -import { commands, logger } from './core/di'; +import { buildServices } from './core/di'; + +const services = buildServices(); +const { logger, commands } = services; const program = new Command(); program.version(packageJson.version); @@ -37,7 +40,9 @@ program.addHelpText('beforeAll', pc.cyanBright(logo)); program.description( "Crystallize CLI helps you manage your Crystallize tenant(s) and improve your DX.\nWe've got your back(end)!\n 🤜✨🤛.", ); +program.addOption(new Option('-e, --env ', 'Environment to use').choices(['staging', 'production'])); commands.forEach((command) => { + // command.addOption(new Option('-e, --env ', 'Environment to use').choices(['staging', 'production'])); command.configureHelp(helpStyling); program.addCommand(command); }); @@ -60,7 +65,7 @@ try { } else { logger.fatal(`Unknown error.`); } - console.error(exception); + logger.debug(exception); logMemory(); process.exit(1); }