From 30aaa3e385ce1cca0957915a86ba6361f0887963 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Thu, 1 Jun 2023 13:17:18 +0300 Subject: [PATCH] refactor: Convert lib/config to TypeScript --- src/lib/cli/prompt.ts | 2 - src/lib/config/software.generated.d.ts | 19 ++++ src/lib/config/{software.js => software.ts} | 120 ++++++++++++++------ src/types.d.ts | 21 ++++ 4 files changed, 125 insertions(+), 37 deletions(-) create mode 100644 src/lib/config/software.generated.d.ts rename src/lib/config/{software.js => software.ts} (70%) create mode 100644 src/types.d.ts diff --git a/src/lib/cli/prompt.ts b/src/lib/cli/prompt.ts index b7e293d14d..335a7fb13a 100644 --- a/src/lib/cli/prompt.ts +++ b/src/lib/cli/prompt.ts @@ -1,5 +1,3 @@ -// @flow - /** * External dependencies */ diff --git a/src/lib/config/software.generated.d.ts b/src/lib/config/software.generated.d.ts new file mode 100644 index 0000000000..d1590623b7 --- /dev/null +++ b/src/lib/config/software.generated.d.ts @@ -0,0 +1,19 @@ +import * as Types from '../../../graphqlTypes'; + +export type UpdateSoftwareSettingsMutationVariables = Types.Exact<{ + appId: Types.Scalars['Int']['input']; + envId: Types.Scalars['Int']['input']; + component: Types.Scalars['String']['input']; + version: Types.Scalars['String']['input']; +}>; + + +export type UpdateSoftwareSettingsMutation = { __typename?: 'Mutation', updateSoftwareSettings?: { __typename?: 'AppEnvironmentSoftwareSettings', php?: { __typename?: 'AppEnvironmentSoftwareSettingsSoftware' } | null, wordpress?: { __typename?: 'AppEnvironmentSoftwareSettingsSoftware' } | null, muplugins?: { __typename?: 'AppEnvironmentSoftwareSettingsSoftware' } | null, nodejs?: { __typename?: 'AppEnvironmentSoftwareSettingsSoftware' } | null } | null }; + +export type UpdateJobQueryVariables = Types.Exact<{ + appId: Types.Scalars['Int']['input']; + envId: Types.Scalars['Int']['input']; +}>; + + +export type UpdateJobQuery = { __typename?: 'Query', app?: { __typename?: 'App', environments?: Array<{ __typename?: 'AppEnvironment', jobs?: Array<{ __typename?: 'Job', type?: string | null, completedAt?: string | null, createdAt?: string | null, inProgressLock?: boolean | null, progress?: { __typename?: 'JobProgress', status?: string | null, steps?: Array<{ __typename?: 'JobProgressStep', step?: string | null, name?: string | null, status?: string | null } | null> | null } | null } | { __typename?: 'PrimaryDomainSwitchJob', type?: string | null, completedAt?: string | null, createdAt?: string | null, inProgressLock?: boolean | null, progress?: { __typename?: 'JobProgress', status?: string | null, steps?: Array<{ __typename?: 'JobProgressStep', step?: string | null, name?: string | null, status?: string | null } | null> | null } | null } | null> | null } | null> | null } | null }; diff --git a/src/lib/config/software.js b/src/lib/config/software.ts similarity index 70% rename from src/lib/config/software.js rename to src/lib/config/software.ts index ac954053ab..75afc761f1 100644 --- a/src/lib/config/software.js +++ b/src/lib/config/software.ts @@ -1,8 +1,7 @@ -// @flow - /** * External dependencies */ +import { setTimeout } from 'node:timers/promises'; import { Select, Confirm } from 'enquirer'; import gql from 'graphql-tag'; import debugLib from 'debug'; @@ -13,6 +12,8 @@ import debugLib from 'debug'; import { isAppNodejs, isAppWordPress } from '../app'; import API from '../api'; import UserError from '../user-error'; +import { UpdateJobQueryVariables } from './software.generated'; +import { JobInterface, Query } from '../../graphqlTypes'; const UPDATE_PROGRESS_POLL_INTERVAL = 5; const debug = debugLib( '@automattic/vip:bin:config-software' ); @@ -131,11 +132,44 @@ const COMPONENT_NAMES = { nodejs: 'Node.js', }; +type ComponentName = keyof typeof COMPONENT_NAMES; + const MANAGED_OPTION_KEY = 'managed_latest'; -const _optionsForVersion = softwareSettings => { +interface SoftwareSetting { + options: Option[]; + current: { + version: string; + }; + pinned: boolean; + name: string; + slug: string; +} + +type SoftwareSettings = Record; + +interface Option { + deprecated: boolean; + unstable: boolean; + version: string; +} + +interface VersionChoice { + message: string; + value: string; + disabled?: boolean; +} + +interface VersionChoices { + managed: VersionChoice[]; + supported: VersionChoice[]; + test: VersionChoice[]; + deprecated: VersionChoice[]; +} + +const _optionsForVersion = ( softwareSettings: SoftwareSetting ): VersionChoice[] => { const { options, current, pinned, slug } = softwareSettings; - const versionChoices = { + const versionChoices: VersionChoices = { managed: [], supported: [], test: [], @@ -188,8 +222,8 @@ const _optionsForVersion = softwareSettings => { } ); }; -const _processComponent = async ( appTypeId: number, userProvidedComponent: string | undefined ) => { - const validComponents = []; +const _processComponent = ( appTypeId: number, userProvidedComponent?: ComponentName ): Promise => { + const validComponents: ComponentName[] = []; if ( isAppWordPress( appTypeId ) ) { validComponents.push( 'wordpress', 'php', 'muplugins' ); } else if ( isAppNodejs( appTypeId ) ) { @@ -200,7 +234,8 @@ const _processComponent = async ( appTypeId: number, userProvidedComponent: stri if ( ! validComponents.includes( userProvidedComponent ) ) { throw new UserError( `Component ${ userProvidedComponent } is not supported. Use one of: ${ validComponents.join( ',' ) }` ); } - return userProvidedComponent; + + return Promise.resolve( userProvidedComponent ); } if ( validComponents.length === 0 ) { @@ -208,23 +243,24 @@ const _processComponent = async ( appTypeId: number, userProvidedComponent: stri } if ( validComponents.length === 1 ) { - return validComponents[ 0 ]; + return Promise.resolve( validComponents[ 0 ] ); } const choices = validComponents.map( item => ( { message: COMPONENT_NAMES[ item ], value: item, } ) ); + const select = new Select( { message: 'Component to update', choices, } ); return select.run().catch( () => { throw new UserError( 'Command cancelled by user.' ); - } ); + } ) as Promise; }; -const _processComponentVersion = async ( softwareSettings, component: string, userProvidedVersion: string | undefined ) => { +const _processComponentVersion = ( softwareSettings: SoftwareSettings, component: ComponentName, userProvidedVersion?: string ): Promise => { const versionChoices = _optionsForVersion( softwareSettings[ component ] ); if ( userProvidedVersion ) { @@ -232,7 +268,8 @@ const _processComponentVersion = async ( softwareSettings, component: string, us if ( ! validValues.includes( userProvidedVersion ) ) { throw new UserError( `Version ${ userProvidedVersion } is not supported for ${ COMPONENT_NAMES[ component ] }. Use one of: ${ validValues.join( ',' ) }` ); } - return userProvidedVersion; + + return Promise.resolve( userProvidedVersion ); } const versionSelect = new Select( { @@ -241,25 +278,26 @@ const _processComponentVersion = async ( softwareSettings, component: string, us } ); return versionSelect.run().catch( () => { throw new UserError( 'Command cancelled by user.' ); - } ); + } ) ; }; interface UpdateData { - component: string, - version: string, + component: ComponentName; + version: string; } export interface UpdatePromptOptions { - component?: string, - version?: string, - force?: boolean, + component?: ComponentName; + version?: string; + force?: boolean; } -export const promptForUpdate = async ( appTypeId: number, opts: UpdatePromptOptions, softwareSettings ): Promise => { +export const promptForUpdate = async ( appTypeId: number, opts: UpdatePromptOptions, softwareSettings: SoftwareSettings ): Promise => { const component = await _processComponent( appTypeId, opts.component ); const version = await _processComponentVersion( softwareSettings, component, opts.version ); - const confirm = opts.force || await new Confirm( { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const confirm: boolean = opts.force || await new Confirm( { // NOSONAR message: `Are you sure you want to upgrade ${ COMPONENT_NAMES[ component ] } to ${ version }?`, } ).run().catch( () => { throw new UserError( 'Command cancelled by user.' ); @@ -289,62 +327,74 @@ export const triggerUpdate = async ( variables: TrigerUpdateOptions ) => { return api.mutate( { mutation: updateSoftwareMutation, variables } ); }; -const _getLatestJob = async ( appId: number, envId: number ) => { +const _getLatestJob = async ( appId: number, envId: number ): Promise => { const api = await API(); - const result = await api.query( { query: updateJobQuery, variables: { appId, envId }, fetchPolicy: 'network-only' } ); - const jobs = result?.data?.app?.environments[ 0 ].jobs || []; + const result = await api.query( { query: updateJobQuery, variables: { appId, envId }, fetchPolicy: 'network-only' } ); + const jobs = result.data.app?.environments?.[0]?.jobs ?? []; if ( jobs.length ) { - return jobs.reduce( ( prev, current ) => ( prev.createdAt > current.createdAt ) ? prev : current ); + return jobs.reduce( ( prev, current ) => ( ( prev?.createdAt || '' ) > ( current?.createdAt || '' ) ) ? prev : current ); } return null; }; -const _getCompletedJob = async ( appId: number, envId: number ) => { +const _getCompletedJob = async ( appId: number, envId: number ): Promise => { const latestJob = await _getLatestJob( appId, envId ); debug( 'Latest job result:', latestJob ); - if ( ! latestJob || ! latestJob.inProgressLock ) { + if ( ! latestJob?.inProgressLock ) { return latestJob; } debug( `Sleep for ${ UPDATE_PROGRESS_POLL_INTERVAL } seconds` ); - await new Promise( resolve => setTimeout( resolve, UPDATE_PROGRESS_POLL_INTERVAL * 1000 ) ); - + await setTimeout( UPDATE_PROGRESS_POLL_INTERVAL * 1000 ); return _getCompletedJob( appId, envId ); }; -interface UpdateResult { - ok: boolean; - errorMessage?: string; +interface UpdateResultSuccess { + ok: true; +} + +interface UpdateResultError { + ok: false; + errorMessage: string; } +type UpdateResult = UpdateResultSuccess | UpdateResultError; + export const getUpdateResult = async ( appId: number, envId: number ): Promise => { debug( 'Getting update result', { appId, envId } ); const completedJob = await _getCompletedJob( appId, envId ); - const success = ! completedJob || completedJob?.progress?.status === 'success'; + const success = ! completedJob || completedJob.progress?.status === 'success'; if ( success ) { return { ok: true, }; } - const failedStep = completedJob?.progress?.steps?.find( step => step.status === 'failed' ); - const error = failedStep ? `Failed during step: ${ failedStep.name }` : 'Software update failed'; + const failedStep = completedJob.progress?.steps?.find( step => step?.status === 'failed' ); + const error = failedStep ? `Failed during step: ${ failedStep.name! }` : 'Software update failed'; return { ok: false, errorMessage: error, }; }; -export const formatSoftwareSettings = ( softwareSetting: SoftwareSettings, includes: string[], format: string ) => { +interface FormatSoftwareSettingsResult { + name: string; + slug: string; + version: string; + available_versions?: string | string[]; +} + +export const formatSoftwareSettings = ( softwareSetting: SoftwareSetting, includes: string[], format: string ): FormatSoftwareSettingsResult => { let version = softwareSetting.current.version; if ( softwareSetting.slug === 'wordpress' && ! softwareSetting.pinned ) { version += ' (managed updates)'; } - const result = { + const result: FormatSoftwareSettingsResult = { name: softwareSetting.name, slug: softwareSetting.slug, version, diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000000..1ace1227d0 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,21 @@ +import { ArrayPromptOptions } from 'enquirer'; + +// These types are not 100% correct, but they are close enough for now. +declare module 'enquirer' { + interface ConfirmOption { + name?: string | (() => string); + message: string | (() => string); + initial?: boolean | (() => boolean); + actions?: {'ctrl': {[key: string]: string}} + } + + class Confirm extends NodeJS.EventEmitter { + constructor(option: ConfirmOption); + run: () => Promise; + } + + class Select extends NodeJS.EventEmitter { + constructor(option: ArrayPromptOptions); + run: () => Promise; + } +}