From 7dc6a5301579159e1c3e38976c1097f77d8c18d1 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 26 Sep 2025 09:01:38 +0100 Subject: [PATCH 1/6] feat: delete extensions --- docs/EXTENSION.md | 39 ++++++- src/commands/extension/delete.spec.ts | 155 ++++++++++++++++++++++++++ src/commands/extension/delete.ts | 109 ++++++++++++++++++ 3 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 src/commands/extension/delete.spec.ts create mode 100644 src/commands/extension/delete.ts diff --git a/docs/EXTENSION.md b/docs/EXTENSION.md index 4254abe7..1f4b8707 100644 --- a/docs/EXTENSION.md +++ b/docs/EXTENSION.md @@ -14,6 +14,7 @@ Return to [README.md](../README.md) for information on other command categories. - [Commands](#commands) - [export](#export) - [import](#import) + - [delete](#delete) @@ -43,10 +44,10 @@ dc-cli extension export #### Options -| Option Name | Type | Description | -| --------------- | --------- | ------------------------------------------------------------ | +| Option Name | Type | Description | +| --------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --id | [string] | The ID of an Extension to be exported.
If no --id option is given, all extensions for the hub are exported.
A single --id option may be given to export a single extension.
Multiple --id options may be given to export multiple extensions at the same time. | -| -f
--force | [boolean] | Overwrite extensions without asking. | +| -f
--force | [boolean] | Overwrite extensions without asking. | #### Examples @@ -76,3 +77,35 @@ The import command only uses [common options](#Common Options) `dc-cli extension import ./myDirectory/extension` +### delete + +Deletes extensions from the targeted Dynamic Content hub. + +``` +dc-cli extension delete +``` + +#### Options + +| Option Name | Type | Description | +| --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| --id | [string] | The ID of an Extension to be deleted.
If no --id option is given, all extensions for the hub are deleted.
A single --id option may be given to delete a single extension.
Multiple --id options may be given to delete multiple extensions at the same time. | +| -f
--force | [boolean] | Delete extensions without asking. | + +#### Examples + +##### Delete all extensions from a Hub + +`dc-cli extension delete` + +##### Delete a single extension with the ID of 'foo' + +`dc-cli extension delete foo` + +or + +`dc-cli extension delete --id foo` + +##### Delete multiple extensions with the IDs of 'foo' & 'bar' + +`dc-cli extension delete --id foo --id bar` diff --git a/src/commands/extension/delete.spec.ts b/src/commands/extension/delete.spec.ts new file mode 100644 index 00000000..6ccc71f0 --- /dev/null +++ b/src/commands/extension/delete.spec.ts @@ -0,0 +1,155 @@ +import * as deleteModule from './delete'; +import Yargs from 'yargs/yargs'; +import { builder, coerceLog, LOG_FILENAME, command, handler } from './delete'; +import { getDefaultLogPath } from '../../common/log-helpers'; +import { Extension } from 'dc-management-sdk-js'; +import MockPage from '../../common/dc-management-sdk-js/mock-page'; +import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; +import { FileLog } from '../../common/file-log'; +import { filterExtensionsById } from './export'; + +jest.mock('../../services/dynamic-content-client-factory'); +jest.mock('../../common/log-helpers'); + +describe('delete extensions', () => { + it('should implement an export command', () => { + expect(command).toEqual('delete [id]'); + }); + + describe('builder tests', () => { + it('should configure yargs', () => { + const argv = Yargs(process.argv.slice(2)); + const spyPositional = jest.spyOn(argv, 'positional').mockReturnThis(); + const spyOption = jest.spyOn(argv, 'option').mockReturnThis(); + + builder(argv); + + expect(spyPositional).toHaveBeenCalledWith('id', { + describe: + 'The ID of a the extension to be deleted. If id is not provided, this command will delete ALL extensions in the hub.', + type: 'string' + }); + expect(spyOption).toHaveBeenCalledWith('f', { + type: 'boolean', + boolean: true, + describe: 'If present, there will be no confirmation prompt before deleting the found extensions.' + }); + expect(spyOption).toHaveBeenCalledWith('logFile', { + type: 'string', + default: LOG_FILENAME, + describe: 'Path to a log file to write to.', + coerce: coerceLog + }); + }); + }); + + describe('handler tests', () => { + const yargArgs = { + $0: 'test', + _: ['test'] + }; + + const config = { + clientId: 'client-id', + clientSecret: 'client-id', + hubId: 'hub-id' + }; + + const extensionsToDelete: Extension[] = [ + new Extension({ + id: 'extension-id-1', + name: 'extension-name-1', + label: 'extension-label-1', + status: 'ACTIVE' + }), + new Extension({ + id: 'extension-id-2', + name: 'extension-name-2', + label: 'extension-label-2', + status: 'ACTIVE' + }) + ]; + + let mockGetHub: jest.Mock; + let mockList: jest.Mock; + + const extensionIdsToDelete = (id: unknown) => (id ? (Array.isArray(id) ? id : [id]) : []); + + beforeEach((): void => { + const listResponse = new MockPage(Extension, extensionsToDelete); + mockList = jest.fn().mockResolvedValue(listResponse); + + mockGetHub = jest.fn().mockResolvedValue({ + related: { + extensions: { + list: mockList + } + } + }); + + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub + } + }); + + jest.spyOn(deleteModule, 'processExtensions').mockResolvedValue(); + }); + + it('should use getDefaultLogPath for LOG_FILENAME with process.platform as default', function () { + LOG_FILENAME(); + expect(getDefaultLogPath).toHaveBeenCalledWith('extension', 'delete', process.platform); + }); + + it('should delete all extensions in a hub', async (): Promise => { + const id: string[] | undefined = undefined; + const allExtensions = !id; + const argv = { ...yargArgs, ...config, extensionId: extensionIdsToDelete, logFile: new FileLog() }; + + const filteredExtensionsToDelete = filterExtensionsById(extensionsToDelete, extensionIdsToDelete(id)); + + jest.spyOn(deleteModule, 'handler'); + + await handler(argv); + + expect(mockGetHub).toHaveBeenCalledWith('hub-id'); + expect(mockList).toHaveBeenCalledTimes(1); + expect(mockList).toHaveBeenCalledWith({ size: 100 }); + + expect(deleteModule.processExtensions).toHaveBeenCalledWith( + filteredExtensionsToDelete, + allExtensions, + argv.logFile, + false + ); + }); + + it('should delete an extension by id', async (): Promise => { + const id: string[] | undefined = ['extension-id-2']; + const allExtensions = !id; + const argv = { + ...yargArgs, + ...config, + id, + extensionId: extensionIdsToDelete, + logFile: new FileLog() + }; + + const filteredExtensionsToDelete = filterExtensionsById(extensionsToDelete, extensionIdsToDelete(id)); + + jest.spyOn(deleteModule, 'handler'); + + await handler(argv); + + expect(mockGetHub).toHaveBeenCalledWith('hub-id'); + expect(mockList).toHaveBeenCalledTimes(1); + + expect(deleteModule.processExtensions).toHaveBeenCalledWith( + filteredExtensionsToDelete, + allExtensions, + argv.logFile, + false + ); + }); + }); +}); diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts new file mode 100644 index 00000000..5a2826a3 --- /dev/null +++ b/src/commands/extension/delete.ts @@ -0,0 +1,109 @@ +import { Arguments, Argv } from 'yargs'; +import { FileLog } from '../../common/file-log'; +import { createLog, getDefaultLogPath } from '../../common/log-helpers'; +import { ExportBuilderOptions } from '../../interfaces/export-builder-options.interface'; +import { ConfigurationParameters } from '../configure'; +import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; +import paginator from '../../common/dc-management-sdk-js/paginator'; +import { filterExtensionsById } from './export'; +import { nothingExportedExit as nothingToDeleteExit } from '../../services/export.service'; +import { Extension } from 'dc-management-sdk-js'; +import { asyncQuestion } from '../../common/question-helpers'; + +export const command = 'delete [id]'; + +export const desc = 'Delete Extensions'; + +export const LOG_FILENAME = (platform: string = process.platform): string => + getDefaultLogPath('extension', 'delete', platform); + +export const coerceLog = (logFile: string): FileLog => createLog(logFile, 'Extensions Delete Log'); + +export const builder = (yargs: Argv): void => { + yargs + .positional('id', { + type: 'string', + describe: + 'The ID of a the extension to be deleted. If id is not provided, this command will delete ALL extensions in the hub.' + }) + .alias('f', 'force') + .option('f', { + type: 'boolean', + boolean: true, + describe: 'If present, there will be no confirmation prompt before deleting the found extensions.' + }) + .option('logFile', { + type: 'string', + default: LOG_FILENAME, + describe: 'Path to a log file to write to.', + coerce: coerceLog + }); +}; + +export const processExtensions = async ( + extensionsToDelete: Extension[], + allExtensions: boolean, + logFile: FileLog, + force?: boolean +): Promise => { + const failedExtensions: Extension[] = []; + + const log = logFile.open(); + + if (extensionsToDelete.length === 0) { + nothingToDeleteExit(log, 'No extensions to delete from this hub, exiting.'); + return; + } + + if (!force) { + const yes = await asyncQuestion( + allExtensions + ? `Providing no ID/s will delete ALL extensions! Are you sure you want to do this? (Y/n)\n` + : `${extensionsToDelete.length} extensions will be deleted. Would you like to continue? (Y/n)\n` + ); + if (!yes) { + return; + } + } + + log.appendLine(`Deleting ${extensionsToDelete.length} extensions.`); + + for (const [i, extension] of extensionsToDelete.entries()) { + try { + await extension.related.delete(); + log.appendLine(`Successfully deleted "${extension.label}"`); + } catch (e) { + failedExtensions.push(extension); + extensionsToDelete.splice(i, 1); + log.appendLine(`Failed to delete ${extension.label}: ${e.toString()}`); + } + } + + log.appendLine(`Finished successfully deleting ${extensionsToDelete.length} extensions`); + + if (failedExtensions.length > 0) { + log.appendLine(`Failed to delete ${failedExtensions.length} extensions`); + } + + log.appendLine(`Extension deletion complete`); + + await log.close(); +}; + +export const handler = async ( + argv: Arguments & ConfigurationParameters> +): Promise => { + const { id, logFile, force } = argv; + + const client = dynamicContentClientFactory(argv); + + const allExtensions = !id; + + const hub = await client.hubs.get(argv.hubId); + + const storedExtensions = await paginator(hub.related.extensions.list); + + const idArray: string[] = id ? (Array.isArray(id) ? id : [id]) : []; + const filteredExtensions = filterExtensionsById(storedExtensions, idArray); + await processExtensions(filteredExtensions, allExtensions, logFile, force || false); +}; From 539b5a1257cbdb50000deeba1b49a60ef3138038 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 26 Sep 2025 09:14:18 +0100 Subject: [PATCH 2/6] feat: progress bar --- src/commands/extension/delete.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts index 5a2826a3..54688edc 100644 --- a/src/commands/extension/delete.ts +++ b/src/commands/extension/delete.ts @@ -9,6 +9,7 @@ import { filterExtensionsById } from './export'; import { nothingExportedExit as nothingToDeleteExit } from '../../services/export.service'; import { Extension } from 'dc-management-sdk-js'; import { asyncQuestion } from '../../common/question-helpers'; +import { progressBar } from '../../common/progress-bar/progress-bar'; export const command = 'delete [id]'; @@ -68,10 +69,15 @@ export const processExtensions = async ( log.appendLine(`Deleting ${extensionsToDelete.length} extensions.`); + const progress = progressBar(extensionsToDelete.length, 0, { + title: `Deleting ${extensionsToDelete.length} extensions.` + }); + for (const [i, extension] of extensionsToDelete.entries()) { try { await extension.related.delete(); log.appendLine(`Successfully deleted "${extension.label}"`); + progress.increment(); } catch (e) { failedExtensions.push(extension); extensionsToDelete.splice(i, 1); @@ -79,6 +85,8 @@ export const processExtensions = async ( } } + progress.stop(); + log.appendLine(`Finished successfully deleting ${extensionsToDelete.length} extensions`); if (failedExtensions.length > 0) { From 4567c5fc37a722a556c95ba353caaab36e653827 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 26 Sep 2025 11:44:24 +0100 Subject: [PATCH 3/6] fix: progressbar bug --- src/commands/extension/delete.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts index 54688edc..0e38df39 100644 --- a/src/commands/extension/delete.ts +++ b/src/commands/extension/delete.ts @@ -67,7 +67,7 @@ export const processExtensions = async ( } } - log.appendLine(`Deleting ${extensionsToDelete.length} extensions.`); + log.addComment(`Deleting ${extensionsToDelete.length} extensions.`); const progress = progressBar(extensionsToDelete.length, 0, { title: `Deleting ${extensionsToDelete.length} extensions.` @@ -76,12 +76,13 @@ export const processExtensions = async ( for (const [i, extension] of extensionsToDelete.entries()) { try { await extension.related.delete(); - log.appendLine(`Successfully deleted "${extension.label}"`); + log.addComment(`Successfully deleted "${extension.label}"`); progress.increment(); } catch (e) { failedExtensions.push(extension); extensionsToDelete.splice(i, 1); - log.appendLine(`Failed to delete ${extension.label}: ${e.toString()}`); + log.addComment(`Failed to delete ${extension.label}: ${e.toString()}`); + progress.increment(); } } @@ -93,8 +94,6 @@ export const processExtensions = async ( log.appendLine(`Failed to delete ${failedExtensions.length} extensions`); } - log.appendLine(`Extension deletion complete`); - await log.close(); }; From ba726615975c7048d7a4345934800b109bb69331 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 26 Sep 2025 12:04:37 +0100 Subject: [PATCH 4/6] test: add correct id to argv --- src/commands/extension/delete.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/extension/delete.spec.ts b/src/commands/extension/delete.spec.ts index 6ccc71f0..360ccf4f 100644 --- a/src/commands/extension/delete.spec.ts +++ b/src/commands/extension/delete.spec.ts @@ -104,7 +104,7 @@ describe('delete extensions', () => { it('should delete all extensions in a hub', async (): Promise => { const id: string[] | undefined = undefined; const allExtensions = !id; - const argv = { ...yargArgs, ...config, extensionId: extensionIdsToDelete, logFile: new FileLog() }; + const argv = { ...yargArgs, ...config, id, logFile: new FileLog() }; const filteredExtensionsToDelete = filterExtensionsById(extensionsToDelete, extensionIdsToDelete(id)); @@ -131,7 +131,6 @@ describe('delete extensions', () => { ...yargArgs, ...config, id, - extensionId: extensionIdsToDelete, logFile: new FileLog() }; From ca0d5f5d041b569754586862433c3a37acfdbbba Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 26 Sep 2025 13:27:05 +0100 Subject: [PATCH 5/6] refactor: make reusable function, remove pick --- src/commands/extension/delete.spec.ts | 2 +- src/commands/extension/delete.ts | 8 +++---- src/commands/extension/export.spec.ts | 11 +++++---- src/commands/extension/export.ts | 20 +--------------- src/common/extension/extension-helpers.ts | 24 +++++++++++++++++++ .../delete-extension-builder-options.ts | 6 +++++ 6 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 src/common/extension/extension-helpers.ts create mode 100644 src/interfaces/delete-extension-builder-options.ts diff --git a/src/commands/extension/delete.spec.ts b/src/commands/extension/delete.spec.ts index 360ccf4f..ab96f65f 100644 --- a/src/commands/extension/delete.spec.ts +++ b/src/commands/extension/delete.spec.ts @@ -6,7 +6,7 @@ import { Extension } from 'dc-management-sdk-js'; import MockPage from '../../common/dc-management-sdk-js/mock-page'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { FileLog } from '../../common/file-log'; -import { filterExtensionsById } from './export'; +import { filterExtensionsById } from '../../common/extension/extension-helpers'; jest.mock('../../services/dynamic-content-client-factory'); jest.mock('../../common/log-helpers'); diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts index 0e38df39..b41a01de 100644 --- a/src/commands/extension/delete.ts +++ b/src/commands/extension/delete.ts @@ -1,15 +1,15 @@ import { Arguments, Argv } from 'yargs'; import { FileLog } from '../../common/file-log'; import { createLog, getDefaultLogPath } from '../../common/log-helpers'; -import { ExportBuilderOptions } from '../../interfaces/export-builder-options.interface'; import { ConfigurationParameters } from '../configure'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import paginator from '../../common/dc-management-sdk-js/paginator'; -import { filterExtensionsById } from './export'; import { nothingExportedExit as nothingToDeleteExit } from '../../services/export.service'; import { Extension } from 'dc-management-sdk-js'; import { asyncQuestion } from '../../common/question-helpers'; import { progressBar } from '../../common/progress-bar/progress-bar'; +import { filterExtensionsById } from '../../common/extension/extension-helpers'; +import { DeleteExtensionBuilderOptions } from '../../interfaces/delete-extension-builder-options'; export const command = 'delete [id]'; @@ -98,7 +98,7 @@ export const processExtensions = async ( }; export const handler = async ( - argv: Arguments & ConfigurationParameters> + argv: Arguments ): Promise => { const { id, logFile, force } = argv; @@ -111,6 +111,6 @@ export const handler = async ( const storedExtensions = await paginator(hub.related.extensions.list); const idArray: string[] = id ? (Array.isArray(id) ? id : [id]) : []; - const filteredExtensions = filterExtensionsById(storedExtensions, idArray); + const filteredExtensions = filterExtensionsById(storedExtensions, idArray, true); await processExtensions(filteredExtensions, allExtensions, logFile, force || false); }; diff --git a/src/commands/extension/export.spec.ts b/src/commands/extension/export.spec.ts index d838f729..678eee22 100644 --- a/src/commands/extension/export.spec.ts +++ b/src/commands/extension/export.spec.ts @@ -1,9 +1,9 @@ import * as exportModule from './export'; import * as directoryUtils from '../../common/import/directory-utils'; +import * as extensionHelpers from '../../common/extension/extension-helpers'; import { builder, command, - filterExtensionsById, getExtensionExports, getExportRecordForExtension, handler, @@ -22,6 +22,7 @@ import { FileLog } from '../../common/file-log'; import { streamTableOptions } from '../../common/table/table.consts'; import { createLog, getDefaultLogPath } from '../../common/log-helpers'; import { validateNoDuplicateExtensionNames } from './import'; +import { filterExtensionsById } from '../../common/extension/extension-helpers'; jest.mock('../../services/dynamic-content-client-factory'); jest.mock('./import'); @@ -661,7 +662,7 @@ describe('extension export command', (): void => { const argv = { ...yargArgs, ...config, dir: 'my-dir', extensionId: extensionIdsToExport, logFile: new FileLog() }; const filteredExtensionsToExport = [...extensionsToExport]; - jest.spyOn(exportModule, 'filterExtensionsById').mockReturnValue(filteredExtensionsToExport); + jest.spyOn(extensionHelpers, 'filterExtensionsById').mockReturnValue(filteredExtensionsToExport); await handler(argv); @@ -670,7 +671,7 @@ describe('extension export command', (): void => { expect(mockList).toHaveBeenCalledWith({ size: 100 }); expect(loadJsonFromDirectory).toHaveBeenCalledWith(argv.dir, Extension); expect(validateNoDuplicateExtensionNames).toHaveBeenCalled(); - expect(exportModule.filterExtensionsById).toHaveBeenCalledWith(extensionsToExport, []); + expect(extensionHelpers.filterExtensionsById).toHaveBeenCalledWith(extensionsToExport, []); expect(exportModule.processExtensions).toHaveBeenCalledWith( argv.dir, [], @@ -685,7 +686,7 @@ describe('extension export command', (): void => { const argv = { ...yargArgs, ...config, dir: 'my-dir', id: idsToExport, logFile: new FileLog() }; const filteredExtensionsToExport = [extensionsToExport[1]]; - jest.spyOn(exportModule, 'filterExtensionsById').mockReturnValue(filteredExtensionsToExport); + jest.spyOn(extensionHelpers, 'filterExtensionsById').mockReturnValue(filteredExtensionsToExport); await handler(argv); @@ -693,7 +694,7 @@ describe('extension export command', (): void => { expect(mockList).toHaveBeenCalled(); expect(loadJsonFromDirectory).toHaveBeenCalledWith(argv.dir, Extension); expect(validateNoDuplicateExtensionNames).toHaveBeenCalled(); - expect(exportModule.filterExtensionsById).toHaveBeenCalledWith(extensionsToExport, idsToExport); + expect(extensionHelpers.filterExtensionsById).toHaveBeenCalledWith(extensionsToExport, idsToExport); expect(exportModule.processExtensions).toHaveBeenCalledWith( argv.dir, [], diff --git a/src/commands/extension/export.ts b/src/commands/extension/export.ts index a654d50f..88d1fe4a 100644 --- a/src/commands/extension/export.ts +++ b/src/commands/extension/export.ts @@ -20,6 +20,7 @@ import { ensureDirectoryExists } from '../../common/import/directory-utils'; import { FileLog } from '../../common/file-log'; import { createLog, getDefaultLogPath } from '../../common/log-helpers'; import { validateNoDuplicateExtensionNames } from './import'; +import { filterExtensionsById } from '../../common/extension/extension-helpers'; export const command = 'export '; @@ -71,25 +72,6 @@ interface ExportRecord { readonly extension: Extension; } -export const filterExtensionsById = (listToFilter: Extension[], extensionUriList: string[]): Extension[] => { - if (extensionUriList.length === 0) { - return listToFilter; - } - - const unmatchedExtensionUriList: string[] = extensionUriList.filter( - id => !listToFilter.some(extension => extension.id === id) - ); - if (unmatchedExtensionUriList.length > 0) { - throw new Error( - `The following extension URI(s) could not be found: [${unmatchedExtensionUriList - .map(u => `'${u}'`) - .join(', ')}].\nNothing was exported, exiting.` - ); - } - - return listToFilter.filter(extension => extensionUriList.some(id => extension.id === id)); -}; - export const getExportRecordForExtension = ( extension: Extension, outputDir: string, diff --git a/src/common/extension/extension-helpers.ts b/src/common/extension/extension-helpers.ts new file mode 100644 index 00000000..d021be3a --- /dev/null +++ b/src/common/extension/extension-helpers.ts @@ -0,0 +1,24 @@ +import { Extension } from 'dc-management-sdk-js'; + +export const filterExtensionsById = ( + listToFilter: Extension[], + extensionUriList: string[], + deleteExtensions: boolean = false +): Extension[] => { + if (extensionUriList.length === 0) { + return listToFilter; + } + + const unmatchedExtensionUriList: string[] = extensionUriList.filter( + id => !listToFilter.some(extension => extension.id === id) + ); + if (unmatchedExtensionUriList.length > 0) { + throw new Error( + `The following extension URI(s) could not be found: [${unmatchedExtensionUriList + .map(u => `'${u}'`) + .join(', ')}].\nNothing was ${!deleteExtensions ? 'exported' : 'deleted'}, exiting.` + ); + } + + return listToFilter.filter(extension => extensionUriList.some(id => extension.id === id)); +}; diff --git a/src/interfaces/delete-extension-builder-options.ts b/src/interfaces/delete-extension-builder-options.ts new file mode 100644 index 00000000..c07f7153 --- /dev/null +++ b/src/interfaces/delete-extension-builder-options.ts @@ -0,0 +1,6 @@ +import { FileLog } from '../common/file-log'; + +export interface DeleteExtensionBuilderOptions { + logFile: FileLog; + force?: boolean; +} From bfdca42c22d57f23bce664e0be04fbb7cdc529f9 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 17 Oct 2025 13:01:50 +0100 Subject: [PATCH 6/6] fix: move non process required logic into handler --- src/commands/extension/delete.spec.ts | 22 ++++------ src/commands/extension/delete.ts | 60 +++++++++++++-------------- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/commands/extension/delete.spec.ts b/src/commands/extension/delete.spec.ts index ab96f65f..a620e9c1 100644 --- a/src/commands/extension/delete.spec.ts +++ b/src/commands/extension/delete.spec.ts @@ -7,9 +7,11 @@ import MockPage from '../../common/dc-management-sdk-js/mock-page'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { FileLog } from '../../common/file-log'; import { filterExtensionsById } from '../../common/extension/extension-helpers'; +import * as questionHelpers from '../../common/question-helpers'; jest.mock('../../services/dynamic-content-client-factory'); jest.mock('../../common/log-helpers'); +jest.mock('../../common/question-helpers'); describe('delete extensions', () => { it('should implement an export command', () => { @@ -103,30 +105,25 @@ describe('delete extensions', () => { it('should delete all extensions in a hub', async (): Promise => { const id: string[] | undefined = undefined; - const allExtensions = !id; const argv = { ...yargArgs, ...config, id, logFile: new FileLog() }; const filteredExtensionsToDelete = filterExtensionsById(extensionsToDelete, extensionIdsToDelete(id)); jest.spyOn(deleteModule, 'handler'); + (questionHelpers.asyncQuestion as jest.Mock).mockResolvedValue(true); + await handler(argv); expect(mockGetHub).toHaveBeenCalledWith('hub-id'); expect(mockList).toHaveBeenCalledTimes(1); expect(mockList).toHaveBeenCalledWith({ size: 100 }); - expect(deleteModule.processExtensions).toHaveBeenCalledWith( - filteredExtensionsToDelete, - allExtensions, - argv.logFile, - false - ); + expect(deleteModule.processExtensions).toHaveBeenCalledWith(filteredExtensionsToDelete, argv.logFile); }); it('should delete an extension by id', async (): Promise => { const id: string[] | undefined = ['extension-id-2']; - const allExtensions = !id; const argv = { ...yargArgs, ...config, @@ -138,17 +135,14 @@ describe('delete extensions', () => { jest.spyOn(deleteModule, 'handler'); + (questionHelpers.asyncQuestion as jest.Mock).mockResolvedValue(true); + await handler(argv); expect(mockGetHub).toHaveBeenCalledWith('hub-id'); expect(mockList).toHaveBeenCalledTimes(1); - expect(deleteModule.processExtensions).toHaveBeenCalledWith( - filteredExtensionsToDelete, - allExtensions, - argv.logFile, - false - ); + expect(deleteModule.processExtensions).toHaveBeenCalledWith(filteredExtensionsToDelete, argv.logFile); }); }); }); diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts index b41a01de..b3c329cc 100644 --- a/src/commands/extension/delete.ts +++ b/src/commands/extension/delete.ts @@ -41,34 +41,9 @@ export const builder = (yargs: Argv): void => { }); }; -export const processExtensions = async ( - extensionsToDelete: Extension[], - allExtensions: boolean, - logFile: FileLog, - force?: boolean -): Promise => { +export const processExtensions = async (extensionsToDelete: Extension[], log: FileLog): Promise => { const failedExtensions: Extension[] = []; - const log = logFile.open(); - - if (extensionsToDelete.length === 0) { - nothingToDeleteExit(log, 'No extensions to delete from this hub, exiting.'); - return; - } - - if (!force) { - const yes = await asyncQuestion( - allExtensions - ? `Providing no ID/s will delete ALL extensions! Are you sure you want to do this? (Y/n)\n` - : `${extensionsToDelete.length} extensions will be deleted. Would you like to continue? (Y/n)\n` - ); - if (!yes) { - return; - } - } - - log.addComment(`Deleting ${extensionsToDelete.length} extensions.`); - const progress = progressBar(extensionsToDelete.length, 0, { title: `Deleting ${extensionsToDelete.length} extensions.` }); @@ -88,13 +63,9 @@ export const processExtensions = async ( progress.stop(); - log.appendLine(`Finished successfully deleting ${extensionsToDelete.length} extensions`); - if (failedExtensions.length > 0) { log.appendLine(`Failed to delete ${failedExtensions.length} extensions`); } - - await log.close(); }; export const handler = async ( @@ -111,6 +82,31 @@ export const handler = async ( const storedExtensions = await paginator(hub.related.extensions.list); const idArray: string[] = id ? (Array.isArray(id) ? id : [id]) : []; - const filteredExtensions = filterExtensionsById(storedExtensions, idArray, true); - await processExtensions(filteredExtensions, allExtensions, logFile, force || false); + const extensionsToDelete = filterExtensionsById(storedExtensions, idArray, true); + + const log = logFile.open(); + + if (extensionsToDelete.length === 0) { + nothingToDeleteExit(log, 'No extensions to delete from this hub, exiting.'); + return; + } + + if (!force) { + const yes = await asyncQuestion( + allExtensions + ? `Providing no ID/s will delete ALL extensions! Are you sure you want to do this? (Y/n)\n` + : `${extensionsToDelete.length} extensions will be deleted. Would you like to continue? (Y/n)\n` + ); + if (!yes) { + return; + } + } + + log.addComment(`Deleting ${extensionsToDelete.length} extensions.`); + + await processExtensions(extensionsToDelete, log); + + log.appendLine(`Finished successfully deleting ${extensionsToDelete.length} extensions`); + + await log.close(); };