Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions docs/EXTENSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Return to [README.md](../README.md) for information on other command categories.
- [Commands](#commands)
- [export](#export)
- [import](#import)
- [delete](#delete)

<!-- /MarkdownTOC -->

Expand Down Expand Up @@ -43,10 +44,10 @@ dc-cli extension export <dir>

#### Options

| Option Name | Type | Description |
| --------------- | --------- | ------------------------------------------------------------ |
| Option Name | Type | Description |
| --------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --id | [string] | The ID of an Extension to be exported.<br/>If no --id option is given, all extensions for the hub are exported.<br/>A single --id option may be given to export a single extension.<br/>Multiple --id options may be given to export multiple extensions at the same time. |
| -f<br />--force | [boolean] | Overwrite extensions without asking. |
| -f<br />--force | [boolean] | Overwrite extensions without asking. |

#### Examples

Expand Down Expand Up @@ -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.<br/>If no --id option is given, all extensions for the hub are deleted.<br/>A single --id option may be given to delete a single extension.<br/>Multiple --id options may be given to delete multiple extensions at the same time. |
| -f<br />--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`
148 changes: 148 additions & 0 deletions src/commands/extension/delete.spec.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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<void> => {
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);
});
});
});
112 changes: 112 additions & 0 deletions src/commands/extension/delete.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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<DeleteExtensionBuilderOptions & ConfigurationParameters>
): Promise<void> => {
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();
};
11 changes: 6 additions & 5 deletions src/commands/extension/export.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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');
Expand Down Expand Up @@ -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);

Expand All @@ -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,
[],
Expand All @@ -685,15 +686,15 @@ 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);

expect(mockGetHub).toHaveBeenCalledWith('hub-id');
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,
[],
Expand Down
Loading
Loading