From 4236e48f31124a642a9704b5a30f5fa8dc2754e9 Mon Sep 17 00:00:00 2001 From: Ahmed Sayeed Wasif Date: Wed, 24 Jan 2024 15:39:29 +0600 Subject: [PATCH 1/4] Add step to enable pma --- src/commands/phpmyadmin.ts | 117 ++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/commands/phpmyadmin.ts b/src/commands/phpmyadmin.ts index 33ea93472..96dde0381 100644 --- a/src/commands/phpmyadmin.ts +++ b/src/commands/phpmyadmin.ts @@ -14,7 +14,9 @@ import API, { enableGlobalGraphQLErrorHandling, } from '../lib/api'; import * as exit from '../lib/cli/exit'; +import { ProgressTracker } from '../lib/cli/progress'; import { CommandTracker } from '../lib/tracker'; +import { pollUntil } from '../lib/utils'; export const GENERATE_PHP_MY_ADMIN_URL_MUTATION = gql` mutation GeneratePhpMyAdminAccess($input: GeneratePhpMyAdminAccessInput) { @@ -25,6 +27,26 @@ export const GENERATE_PHP_MY_ADMIN_URL_MUTATION = gql` } `; +export const GET_PHP_MY_ADMIN_STATUS_QUERY = gql` + query PhpMyAdminStatus($appId: Int!, $envId: Int!) { + app(id: $appId) { + environments(id: $envId) { + phpMyAdminStatus { + status + } + } + } + } +`; + +export const ENABLE_PHP_MY_ADMIN_MUTATION = gql` + mutation EnablePhpMyAdmin($input: EnablePhpMyAdminInput) { + enablePHPMyAdmin(input: $input) { + success + } + } +`; + async function generatePhpMyAdminAccess( envId: number ): Promise< string > { // Disable global error handling so that we can handle errors ourselves disableGlobalGraphQLErrorHandling(); @@ -46,16 +68,65 @@ async function generatePhpMyAdminAccess( envId: number ): Promise< string > { return resp?.data?.generatePHPMyAdminAccess?.url as string; } +async function enablePhpMyAdmin( envId: number ): Promise< string > { + // Disable global error handling so that we can handle errors ourselves + disableGlobalGraphQLErrorHandling(); + + const api = await API(); + const resp = await api.mutate( { + mutation: ENABLE_PHP_MY_ADMIN_MUTATION, + variables: { + input: { + environmentId: envId, + }, + }, + } ); + + // Re-enable global error handling + enableGlobalGraphQLErrorHandling(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return resp?.data?.generatePHPMyAdminAccess?.url as string; +} + +async function getPhpMyAdminStatus( appId: number, envId: number ): Promise< string > { + // Disable global error handling so that we can handle errors ourselves + disableGlobalGraphQLErrorHandling(); + + const api = await API(); + + const resp = await api.query( { + query: GET_PHP_MY_ADMIN_STATUS_QUERY, + variables: { appId, envId }, + fetchPolicy: 'network-only', + } ); + + // Re-enable global error handling + enableGlobalGraphQLErrorHandling(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return resp?.data?.app?.environments?.[ 0 ]?.phpMyAdminStatus?.status as string; +} + export class PhpMyAdminCommand { app: App; env: AppEnvironment; silent?: boolean; track: CommandTracker; + steps = { + ENABLE: 'enable', + GENERATE: 'generate', + }; + private progressTracker: ProgressTracker; constructor( app: App, env: AppEnvironment, trackerFn: CommandTracker = async () => {} ) { this.app = app; this.env = env; this.track = trackerFn; + this.progressTracker = new ProgressTracker( [ + { id: this.steps.ENABLE, name: 'Enabling PHPMyAdmin for this site' }, + { id: this.steps.GENERATE, name: 'Generating access link' }, + ] ); } log( msg: string ) { @@ -65,14 +136,44 @@ export class PhpMyAdminCommand { console.log( msg ); } + stopProgressTracker() { + this.progressTracker.print(); + this.progressTracker.stopPrinting(); + } + async openUrl( url: string ) { const { default: open } = await import( 'open' ); void open( url, { wait: false } ); } + async getStatus() { + try { + return await getPhpMyAdminStatus( this.app.id as number, this.env.id as number ); + } catch ( err ) { + exit.withError( + 'Failed to get PhpMyAdmin status. Please try again. If the problem persists, please contact support.' + ); + } + } + + async enablePhpMyAdmin() { + const status = await this.getStatus(); + if ( ! [ 'running', 'enabled' ].includes( status ) ) { + await enablePhpMyAdmin( this.env.id as number ); + await pollUntil( this.getStatus.bind( this ), 1000, ( sts: string ) => sts === 'running' ); + + // Additional 30s for LB routing to be updated + await new Promise( resolve => setTimeout( resolve, 30000 ) ); + } + } + async run( silent = false ) { this.silent = silent; + if ( ! this.app.id ) { + exit.withError( 'No app was specified' ); + } + if ( ! this.env.id ) { exit.withError( 'No environment was specified' ); } @@ -80,12 +181,24 @@ export class PhpMyAdminCommand { const message = 'Note: PHPMyAdmin sessions are read-only. If you run a query that writes to DB, it will fail.'; console.log( chalk.yellow( message ) ); - this.log( 'Generating PhpMyAdmin URL...' ); + + this.progressTracker.startPrinting(); + try { + this.progressTracker.stepRunning( this.steps.ENABLE ); + await this.enablePhpMyAdmin(); + this.progressTracker.stepSuccess( this.steps.ENABLE ); + } catch ( err ) { + this.progressTracker.stepFailed( this.steps.ENABLE ); + exit.withError( 'Failed to enable PhpMyAdmin' ); + } let url; try { + this.progressTracker.stepRunning( this.steps.GENERATE ); url = await generatePhpMyAdminAccess( this.env.id ); + this.progressTracker.stepSuccess( this.steps.GENERATE ); } catch ( err ) { + this.progressTracker.stepFailed( this.steps.GENERATE ); const error = err as Error & { graphQLErrors?: GraphQLFormattedError[]; }; @@ -96,7 +209,9 @@ export class PhpMyAdminCommand { } ); exit.withError( `Failed to generate PhpMyAdmin URL: ${ error.message }` ); } + void this.openUrl( url ); + this.stopProgressTracker(); this.log( 'PhpMyAdmin is opened in your default browser.' ); } } From 89ad2e037d501e758957ce4546aa32c9c4b8cada Mon Sep 17 00:00:00 2001 From: Sarosh Aga Date: Wed, 24 Jan 2024 20:40:53 +0400 Subject: [PATCH 2/4] Refactor for clarity - make function name clearer - add return types --- src/commands/phpmyadmin.ts | 45 +++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/commands/phpmyadmin.ts b/src/commands/phpmyadmin.ts index 96dde0381..40d2c7280 100644 --- a/src/commands/phpmyadmin.ts +++ b/src/commands/phpmyadmin.ts @@ -1,8 +1,14 @@ /** * External dependencies */ +import { + ApolloClient, + ApolloQueryResult, + FetchResult, + NormalizedCacheObject, +} from '@apollo/client'; import chalk from 'chalk'; -import { GraphQLFormattedError } from 'graphql'; +import { DocumentNode, GraphQLFormattedError } from 'graphql'; import gql from 'graphql-tag'; /** @@ -18,7 +24,7 @@ import { ProgressTracker } from '../lib/cli/progress'; import { CommandTracker } from '../lib/tracker'; import { pollUntil } from '../lib/utils'; -export const GENERATE_PHP_MY_ADMIN_URL_MUTATION = gql` +export const GENERATE_PHP_MY_ADMIN_URL_MUTATION: DocumentNode = gql` mutation GeneratePhpMyAdminAccess($input: GeneratePhpMyAdminAccessInput) { generatePHPMyAdminAccess(input: $input) { expiresAt @@ -27,7 +33,7 @@ export const GENERATE_PHP_MY_ADMIN_URL_MUTATION = gql` } `; -export const GET_PHP_MY_ADMIN_STATUS_QUERY = gql` +export const GET_PHP_MY_ADMIN_STATUS_QUERY: DocumentNode = gql` query PhpMyAdminStatus($appId: Int!, $envId: Int!) { app(id: $appId) { environments(id: $envId) { @@ -39,7 +45,7 @@ export const GET_PHP_MY_ADMIN_STATUS_QUERY = gql` } `; -export const ENABLE_PHP_MY_ADMIN_MUTATION = gql` +export const ENABLE_PHP_MY_ADMIN_MUTATION: DocumentNode = gql` mutation EnablePhpMyAdmin($input: EnablePhpMyAdminInput) { enablePHPMyAdmin(input: $input) { success @@ -51,8 +57,9 @@ async function generatePhpMyAdminAccess( envId: number ): Promise< string > { // Disable global error handling so that we can handle errors ourselves disableGlobalGraphQLErrorHandling(); - const api = await API(); - const resp = await api.mutate( { + const api: ApolloClient< NormalizedCacheObject > = await API(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resp: FetchResult< any, Record< string, any >, Record< string, any > > = await api.mutate( { mutation: GENERATE_PHP_MY_ADMIN_URL_MUTATION, variables: { input: { @@ -68,12 +75,13 @@ async function generatePhpMyAdminAccess( envId: number ): Promise< string > { return resp?.data?.generatePHPMyAdminAccess?.url as string; } -async function enablePhpMyAdmin( envId: number ): Promise< string > { +async function enable( envId: number ): Promise< string > { // Disable global error handling so that we can handle errors ourselves disableGlobalGraphQLErrorHandling(); - const api = await API(); - const resp = await api.mutate( { + const api: ApolloClient< NormalizedCacheObject > = await API(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resp: FetchResult< any, Record< string, any >, Record< string, any > > = await api.mutate( { mutation: ENABLE_PHP_MY_ADMIN_MUTATION, variables: { input: { @@ -93,9 +101,10 @@ async function getPhpMyAdminStatus( appId: number, envId: number ): Promise< str // Disable global error handling so that we can handle errors ourselves disableGlobalGraphQLErrorHandling(); - const api = await API(); + const api: ApolloClient< NormalizedCacheObject > = await API(); - const resp = await api.query( { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resp: ApolloQueryResult< any > = await api.query( { query: GET_PHP_MY_ADMIN_STATUS_QUERY, variables: { appId, envId }, fetchPolicy: 'network-only', @@ -129,24 +138,24 @@ export class PhpMyAdminCommand { ] ); } - log( msg: string ) { + log( msg: string ): void { if ( this.silent ) { return; } console.log( msg ); } - stopProgressTracker() { + stopProgressTracker(): void { this.progressTracker.print(); this.progressTracker.stopPrinting(); } - async openUrl( url: string ) { + async openUrl( url: string ): Promise< void > { const { default: open } = await import( 'open' ); void open( url, { wait: false } ); } - async getStatus() { + async getStatus(): Promise< string > { try { return await getPhpMyAdminStatus( this.app.id as number, this.env.id as number ); } catch ( err ) { @@ -156,10 +165,10 @@ export class PhpMyAdminCommand { } } - async enablePhpMyAdmin() { + async enablePhpMyAdmin(): Promise< void > { const status = await this.getStatus(); if ( ! [ 'running', 'enabled' ].includes( status ) ) { - await enablePhpMyAdmin( this.env.id as number ); + await enable( this.env.id as number ); await pollUntil( this.getStatus.bind( this ), 1000, ( sts: string ) => sts === 'running' ); // Additional 30s for LB routing to be updated @@ -167,7 +176,7 @@ export class PhpMyAdminCommand { } } - async run( silent = false ) { + async run( silent = false ): Promise< void > { this.silent = silent; if ( ! this.app.id ) { From ea4e936fd0cd135b5828fc15c6dbe73d7cfe2dd4 Mon Sep 17 00:00:00 2001 From: Sarosh Aga Date: Wed, 24 Jan 2024 20:41:07 +0400 Subject: [PATCH 3/4] Fix and improve unit test --- __tests__/commands/phpmyadmin.ts | 48 ++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/__tests__/commands/phpmyadmin.ts b/__tests__/commands/phpmyadmin.ts index e115c376e..9a584db17 100644 --- a/__tests__/commands/phpmyadmin.ts +++ b/__tests__/commands/phpmyadmin.ts @@ -11,7 +11,7 @@ import { PhpMyAdminCommand } from '../../src/commands/phpmyadmin'; import API from '../../src/lib/api'; import { CommandTracker } from '../../src/lib/tracker'; -const mutationMock = jest.fn( async () => { +const generatePMAAccessMutationMock = jest.fn( async () => { return Promise.resolve( { data: { generatePHPMyAdminAccess: { @@ -21,10 +21,37 @@ const mutationMock = jest.fn( async () => { } ); } ); +const enablePMAMutationMock = jest.fn( async () => { + return Promise.resolve( { + data: { + enablePHPMyAdmin: { + success: true, + }, + }, + } ); +} ); + +const pmaEnabledQueryMockTrue = jest.fn( async () => { + return Promise.resolve( { + data: { + app: { + environments: [ + { + phpMyAdminStatus: { + status: 'enabled', + }, + }, + ], + }, + }, + } ); +} ); + jest.mock( '../../src/lib/api' ); jest.mocked( API ).mockImplementation( () => { return Promise.resolve( { - mutate: mutationMock, + mutate: generatePMAAccessMutationMock, + query: pmaEnabledQueryMockTrue, } as any ); } ); @@ -42,9 +69,18 @@ describe( 'commands/PhpMyAdminCommand', () => { openUrl.mockReset(); } ); - it( 'should generate a URL by calling the right mutation', async () => { + it( 'should open the generated URL in browser', async () => { await cmd.run(); - expect( mutationMock ).toHaveBeenCalledWith( { + expect( pmaEnabledQueryMockTrue ).toHaveBeenCalledWith( { + query: expect.anything(), + variables: { + appId: 123, + envId: 456, + }, + fetchPolicy: 'network-only', + } ); + expect( enablePMAMutationMock ).not.toHaveBeenCalled(); + expect( generatePMAAccessMutationMock ).toHaveBeenCalledWith( { mutation: expect.anything(), variables: { input: { @@ -52,10 +88,6 @@ describe( 'commands/PhpMyAdminCommand', () => { }, }, } ); - } ); - - it( 'should open the generated URL in browser', async () => { - await cmd.run(); expect( openUrl ).toHaveBeenCalledWith( 'http://test-url.com' ); } ); } ); From af1215aadbaf0b7001efa9eda834256f021a2b9b Mon Sep 17 00:00:00 2001 From: Sarosh Aga Date: Wed, 24 Jan 2024 20:52:20 +0400 Subject: [PATCH 4/4] Make function names even clearer --- src/commands/phpmyadmin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/phpmyadmin.ts b/src/commands/phpmyadmin.ts index 40d2c7280..5bb580a7b 100644 --- a/src/commands/phpmyadmin.ts +++ b/src/commands/phpmyadmin.ts @@ -75,7 +75,7 @@ async function generatePhpMyAdminAccess( envId: number ): Promise< string > { return resp?.data?.generatePHPMyAdminAccess?.url as string; } -async function enable( envId: number ): Promise< string > { +async function enablePhpMyAdmin( envId: number ): Promise< string > { // Disable global error handling so that we can handle errors ourselves disableGlobalGraphQLErrorHandling(); @@ -165,10 +165,10 @@ export class PhpMyAdminCommand { } } - async enablePhpMyAdmin(): Promise< void > { + async maybeEnablePhpMyAdmin(): Promise< void > { const status = await this.getStatus(); if ( ! [ 'running', 'enabled' ].includes( status ) ) { - await enable( this.env.id as number ); + await enablePhpMyAdmin( this.env.id as number ); await pollUntil( this.getStatus.bind( this ), 1000, ( sts: string ) => sts === 'running' ); // Additional 30s for LB routing to be updated @@ -194,7 +194,7 @@ export class PhpMyAdminCommand { this.progressTracker.startPrinting(); try { this.progressTracker.stepRunning( this.steps.ENABLE ); - await this.enablePhpMyAdmin(); + await this.maybeEnablePhpMyAdmin(); this.progressTracker.stepSuccess( this.steps.ENABLE ); } catch ( err ) { this.progressTracker.stepFailed( this.steps.ENABLE );