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..a620e9c1
--- /dev/null
+++ b/src/commands/extension/delete.spec.ts
@@ -0,0 +1,148 @@
+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 '../../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', () => {
+ 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 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, argv.logFile);
+ });
+
+ it('should delete an extension by id', async (): Promise => {
+ const id: string[] | undefined = ['extension-id-2'];
+ 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(deleteModule.processExtensions).toHaveBeenCalledWith(filteredExtensionsToDelete, argv.logFile);
+ });
+ });
+});
diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts
new file mode 100644
index 00000000..b3c329cc
--- /dev/null
+++ b/src/commands/extension/delete.ts
@@ -0,0 +1,112 @@
+import { Arguments, Argv } from 'yargs';
+import { FileLog } from '../../common/file-log';
+import { createLog, getDefaultLogPath } from '../../common/log-helpers';
+import { ConfigurationParameters } from '../configure';
+import dynamicContentClientFactory from '../../services/dynamic-content-client-factory';
+import paginator from '../../common/dc-management-sdk-js/paginator';
+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]';
+
+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[], log: FileLog): Promise => {
+ const failedExtensions: Extension[] = [];
+
+ const progress = progressBar(extensionsToDelete.length, 0, {
+ title: `Deleting ${extensionsToDelete.length} extensions.`
+ });
+
+ for (const [i, extension] of extensionsToDelete.entries()) {
+ try {
+ await extension.related.delete();
+ log.addComment(`Successfully deleted "${extension.label}"`);
+ progress.increment();
+ } catch (e) {
+ failedExtensions.push(extension);
+ extensionsToDelete.splice(i, 1);
+ log.addComment(`Failed to delete ${extension.label}: ${e.toString()}`);
+ progress.increment();
+ }
+ }
+
+ progress.stop();
+
+ if (failedExtensions.length > 0) {
+ log.appendLine(`Failed to delete ${failedExtensions.length} extensions`);
+ }
+};
+
+export const handler = async (
+ argv: Arguments
+): 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 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();
+};
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;
+}