diff --git a/README.md b/README.md
index c3f96dc3..1f1db80b 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ Run `dc-cli --help` to get a list of available commands.
- [hub](#hub)
- [job](#job)
- [linked-content-repository](#linked-content-repository)
+ - [webhook](#webhook)
- [Building the CLI](#building-the-cli)
- [Required permissions](#required-permissions)
@@ -137,7 +138,7 @@ Before importing, copying, or moving content you must ensure that a valid [conte
### extension
-This category includes interactions with Dynamic Content's UI Extensions, and can be used to export and import extensions from an individual hub.
+This category includes interactions with Dynamic Content's UI Extensions, and can be used to export, import and delete extensions from an individual hub.
[View commands for **extension**](docs/EXTENSION.md)
@@ -191,6 +192,12 @@ These commands can be used to list multiple linked content repository.
[View commands for **linked-content-repository**](docs/LINKED-CONTENT-REPOSITORY.md)
+### webhook
+
+This category includes interactions with Dynamic Content's UI Webhooks, and can be used to export, import and delete webhooks from an individual hub.
+
+[View commands for **webhook**](docs/WEBHOOK.md)
+
## Building the CLI
We have included some NPM scripts to help create various installations of the CLI.
diff --git a/docs/EXTENSION.md b/docs/EXTENSION.md
index 1f4b8707..d1c690c5 100644
--- a/docs/EXTENSION.md
+++ b/docs/EXTENSION.md
@@ -44,10 +44,11 @@ dc-cli extension export
#### Options
-| 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. |
+| 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. |
+| --logFile | [string]
[default: (generated-value)] | Path to a log file to write to. |
#### Examples
@@ -87,10 +88,11 @@ 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. |
+| 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. |
+| --logFile | [string]
[default: (generated-value)] | Path to a log file to write to. |
#### Examples
diff --git a/docs/WEBHOOK.md b/docs/WEBHOOK.md
new file mode 100644
index 00000000..2fe4ec75
--- /dev/null
+++ b/docs/WEBHOOK.md
@@ -0,0 +1,144 @@
+# webhook
+
+## Description
+
+The **webhook** command category includes a number of interactions with webhooks.
+
+These commands can be used to export, import and delete webhooks from an individual hub.
+
+Run `dc-cli webhook --help` to get a list of available commands.
+
+Return to [README.md](../README.md) for information on other command categories.
+
+
+
+- [Common Options](#common-options)
+- [Commands](#commands)
+ - [export](#export)
+ - [import](#import)
+ - [delete](#delete)
+
+
+
+## Common Options
+
+The following options are available for all **webhook** commands.
+
+| Option Name | Type | Description |
+| -------------- | ---------------------------------------------------------- | -------------------------------- |
+| --version | [boolean] | Show version number |
+| --clientId | [string]
[required] | Client ID for the source hub |
+| --clientSecret | [string]
[required] | Client secret for the source hub |
+| --hubId | [string]
[required] | Hub ID for the source hub |
+| --config | [string]
[default: "~/.amplience/dc-cli-config.json"] | Path to JSON config file |
+| --help | [boolean] | Show help |
+
+## Commands
+
+### export
+
+Exports webhooks from the targeted Dynamic Content hub into a folder called **exported_webhooks** at the user specified file path.
+
+**Note**: No secret or auth header values will be exported.
+
+```
+dc-cli webhook export
+```
+
+#### Options
+
+| Option Name | Type | Description |
+| ---------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| --id | [string] | The ID of the webhook to be exported.
If no --id option is given, all webhooks for the hub are exported.
A single --id option may be given to export a single webhook.
Multiple --id options may be given to export multiple webhooks at the same time. |
+| --logFile | [string]
[default: (generated-value)] | Path to a log file to write to. |
+| -s
--silent | [boolean] | If present, no log file will be produced. |
+| -f
--force | [boolean] | Export webhooks without asking. |
+
+#### Examples
+
+##### Export all webhooks from a hub
+
+`dc-cli webhook export ./myDirectory/content`
+
+##### Export a single webhook from a hub
+
+`dc-cli webhook export ./myDirectory/content --id 1111111111`
+
+##### Export multiple webhooks from a hub
+
+`dc-cli webhook export ./myDirectory/content --id 1111111111 --id 2222222222`
+
+### import
+
+Imports webhooks from the specified filesystem location to the targeted Dynamic Content hub.
+
+**Note**: The following values will be stripped out / not included during the import:
+
+- **secret** - this will be recreated for the webhook in the destination hub during import.
+- **createdDate** - this will be assigned during import (if webhook is being created).
+- **lastModifiedDate** - this will be assigned during import (if webhook is being updated).
+- **any header objects that are secrets** - these need to be manually assigned for the webhook in the destination hub.
+
+Please see [the content-management API reference for Webhooks](https://amplience.com/developers/docs/apis/content-management-reference/#tag/Webhooks) for more information.
+
+For any **customPayload** the following property values will be replaced by those in the destination hub:
+
+- account
+- stagingEnvironment
+
+```
+dc-cli webhook import
+```
+
+#### Options
+
+| Option Name | Type | Description |
+| ---------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| --mapFile | [string] | Mapping file to use when updating content that already exists.
Updated with any new mappings that are generated. If not present, will be created.
For more information, see [mapping files](#MAPPING-FILES). |
+| --logFile | [string]
[default: (generated-value)] | Path to a log file to write to. |
+| -s
--silent | [boolean] | If present, no log file will be produced. |
+| -f
--force | [boolean] | Overwrite webhooks without asking. |
+
+#### Examples
+
+##### Import content from the filesystem
+
+`dc-cli webhook import ./myDirectory/webhooks`
+
+##### Specify a mapping file when importing
+
+`dc-cli webhook import ./myDirectory/webhooks --mapFile ./myDirectory/mappingFile.json`
+
+### delete
+
+Deletes webhooks from the targeted Dynamic Content hub.
+
+```
+dc-cli webhook delete
+```
+
+#### Options
+
+| Option Name | Type | Description |
+| --------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| --id | [string] | The ID of an Webhook to be deleted.
If no --id option is given, all webhooks for the hub are deleted.
A single --id option may be given to delete a single webhook.
Multiple --id options may be given to delete multiple webhooks at the same time. |
+| -f
--force | [boolean] | Delete webhooks without asking. |
+| --logFile | [string]
[default: (generated-value)] | Path to a log file to write to. |
+
+#### Examples
+
+##### Delete all webhooks from a Hub
+
+`dc-cli webhook delete`
+
+##### Delete a single webhook with the ID of 'foo'
+
+`dc-cli webhook delete foo`
+
+or
+
+`dc-cli webhook delete --id foo`
+
+##### Delete multiple webhooks with the IDs of 'foo' & 'bar'
+
+`dc-cli webhook delete --id foo --id bar`
diff --git a/src/__snapshots__/cli.spec.ts.snap b/src/__snapshots__/cli.spec.ts.snap
index 8f75dd6b..a968b08e 100644
--- a/src/__snapshots__/cli.spec.ts.snap
+++ b/src/__snapshots__/cli.spec.ts.snap
@@ -16,6 +16,7 @@ Commands:
dc-cli linked-content-repository Linked Content Repository
dc-cli search-index Search Index
dc-cli settings Settings
+ dc-cli webhook Webhook
Options:
--help Show help [boolean]
diff --git a/src/commands/extension/delete.spec.ts b/src/commands/extension/delete.spec.ts
index a620e9c1..b677c3a5 100644
--- a/src/commands/extension/delete.spec.ts
+++ b/src/commands/extension/delete.spec.ts
@@ -28,7 +28,7 @@ describe('delete extensions', () => {
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.',
+ 'The ID of 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', {
diff --git a/src/commands/extension/delete.ts b/src/commands/extension/delete.ts
index b3c329cc..7ce147eb 100644
--- a/src/commands/extension/delete.ts
+++ b/src/commands/extension/delete.ts
@@ -25,7 +25,7 @@ export const builder = (yargs: Argv): void => {
.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.'
+ 'The ID of the extension to be deleted. If id is not provided, this command will delete ALL extensions in the hub.'
})
.alias('f', 'force')
.option('f', {
@@ -92,10 +92,11 @@ export const handler = async (
}
if (!force) {
+ const baseMessage = 'This action cannot be undone. Are you sure you want to continue? (Y/n)\n';
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`
+ ? `Providing no ID/s will permanently delete ALL extensions! ${baseMessage}`
+ : `${extensionsToDelete.length} extensions will be permanently deleted. ${baseMessage}`
);
if (!yes) {
return;
diff --git a/src/commands/webhook.ts b/src/commands/webhook.ts
new file mode 100644
index 00000000..e397573e
--- /dev/null
+++ b/src/commands/webhook.ts
@@ -0,0 +1,18 @@
+import { Argv } from 'yargs';
+import { readConfig } from '../cli';
+import YargsCommandBuilderOptions from '../common/yargs/yargs-command-builder-options';
+import { configureCommandOptions } from './configure';
+
+export const command = 'webhook';
+
+export const desc = 'Webhook';
+
+export const builder = (yargs: Argv): Argv =>
+ yargs
+ .commandDir('webhook', YargsCommandBuilderOptions)
+ .options(configureCommandOptions)
+ .config('config', readConfig)
+ .demandCommand()
+ .help();
+
+export const handler = (): void => {};
diff --git a/src/commands/webhook/delete.spec.ts b/src/commands/webhook/delete.spec.ts
new file mode 100644
index 00000000..eb9f9381
--- /dev/null
+++ b/src/commands/webhook/delete.spec.ts
@@ -0,0 +1,139 @@
+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 { Webhook } 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 * 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 webhooks', () => {
+ 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 the webhook to be deleted. If id is not provided, this command will delete ALL webhooks 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 webhooks.'
+ });
+ 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 webhooksToDelete: Webhook[] = [
+ new Webhook({
+ id: 'webhook-id-1',
+ label: 'webhook-label-1'
+ }),
+ new Webhook({
+ id: 'webhook-id-2',
+ label: 'webhook-label-2'
+ })
+ ];
+
+ let mockGetHub: jest.Mock;
+ let mockList: jest.Mock;
+ let mockGet: jest.Mock;
+
+ beforeEach((): void => {
+ const listResponse = new MockPage(Webhook, webhooksToDelete);
+ mockList = jest.fn().mockResolvedValue(listResponse);
+ mockGet = jest.fn().mockResolvedValue(webhooksToDelete[1]);
+
+ mockGetHub = jest.fn().mockResolvedValue({
+ related: {
+ webhooks: {
+ list: mockList,
+ get: mockGet
+ }
+ }
+ });
+
+ (dynamicContentClientFactory as jest.Mock).mockReturnValue({
+ hubs: {
+ get: mockGetHub
+ }
+ });
+
+ jest.spyOn(deleteModule, 'processWebhooks').mockResolvedValue({ failedWebhooks: [] });
+ });
+
+ it('should use getDefaultLogPath for LOG_FILENAME with process.platform as default', function () {
+ LOG_FILENAME();
+ expect(getDefaultLogPath).toHaveBeenCalledWith('webhook', 'delete', process.platform);
+ });
+
+ it('should delete all webhooks in a hub', async (): Promise => {
+ const argv = { ...yargArgs, ...config, logFile: new FileLog() };
+
+ 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.processWebhooks).toHaveBeenCalledWith(webhooksToDelete, argv.logFile);
+ });
+
+ it('should delete a webhook by id', async (): Promise => {
+ const id: string[] | undefined = ['webhook-id-2'];
+ const argv = {
+ ...yargArgs,
+ ...config,
+ id,
+ logFile: new FileLog()
+ };
+
+ jest.spyOn(deleteModule, 'handler');
+
+ (questionHelpers.asyncQuestion as jest.Mock).mockResolvedValue(true);
+
+ await handler(argv);
+
+ expect(mockGetHub).toHaveBeenCalledWith('hub-id');
+ expect(mockGet).toHaveBeenCalledTimes(1);
+
+ expect(deleteModule.processWebhooks).toHaveBeenCalledWith([webhooksToDelete[1]], argv.logFile);
+ });
+ });
+});
diff --git a/src/commands/webhook/delete.ts b/src/commands/webhook/delete.ts
new file mode 100644
index 00000000..86569bba
--- /dev/null
+++ b/src/commands/webhook/delete.ts
@@ -0,0 +1,115 @@
+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 { nothingExportedExit as nothingToDeleteExit } from '../../services/export.service';
+import { Webhook } from 'dc-management-sdk-js';
+import { asyncQuestion } from '../../common/question-helpers';
+import { progressBar } from '../../common/progress-bar/progress-bar';
+import { DeleteWebhookBuilderOptions } from '../../interfaces/delete-webhook-builder-options';
+import { getWebhooksByIds } from '../../common/webhooks/get-webhooks-by-ids';
+import { getAllWebhooks } from '../../common/webhooks/get-all-webhook';
+
+export const command = 'delete [id]';
+
+export const desc = 'Delete Webhook';
+
+export const LOG_FILENAME = (platform: string = process.platform): string =>
+ getDefaultLogPath('webhook', 'delete', platform);
+
+export const coerceLog = (logFile: string): FileLog => createLog(logFile, 'Webhook Delete Log');
+
+export const builder = (yargs: Argv): void => {
+ yargs
+ .positional('id', {
+ type: 'string',
+ describe:
+ 'The ID of the webhook to be deleted. If id is not provided, this command will delete ALL webhooks 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 webhooks.'
+ })
+ .option('logFile', {
+ type: 'string',
+ default: LOG_FILENAME,
+ describe: 'Path to a log file to write to.',
+ coerce: coerceLog
+ });
+};
+
+export const processWebhooks = async (
+ webhooksToDelete: Webhook[],
+ log: FileLog
+): Promise<{ failedWebhooks: Webhook[] }> => {
+ const failedWebhooks: Webhook[] = [];
+
+ const progress = progressBar(webhooksToDelete.length, 0, {
+ title: `Deleting ${webhooksToDelete.length} webhook/s.`
+ });
+
+ for (const webhook of webhooksToDelete) {
+ try {
+ await webhook.related.delete();
+ log.addComment(`Successfully deleted "${webhook.label}"`);
+ progress.increment();
+ } catch (e) {
+ failedWebhooks.push(webhook);
+ log.addComment(`Failed to delete ${webhook.label}: ${e.toString()}`);
+ progress.increment();
+ }
+ }
+
+ progress.stop();
+
+ return { failedWebhooks };
+};
+
+export const handler = async (
+ argv: Arguments
+): Promise => {
+ const { id, logFile, force } = argv;
+ const log = logFile.open();
+ const client = dynamicContentClientFactory(argv);
+ const allWebhooks = !id;
+ const hub = await client.hubs.get(argv.hubId);
+ let ids: string[] = [];
+
+ if (id) {
+ ids = Array.isArray(id) ? id : [id];
+ }
+
+ const webhooksToDelete = ids.length > 0 ? await getWebhooksByIds(hub, ids) : await getAllWebhooks(hub);
+
+ if (webhooksToDelete.length === 0) {
+ nothingToDeleteExit(log, 'No webhooks to delete from this hub, exiting.');
+ return;
+ }
+
+ if (!force) {
+ const baseMessage = 'This action cannot be undone. Are you sure you want to continue? (Y/n)\n';
+ const yes = await asyncQuestion(
+ allWebhooks
+ ? `Providing no ID/s will permanently delete ALL webhooks! ${baseMessage}`
+ : `${webhooksToDelete.length} webhook/s will be permanently deleted. ${baseMessage}`
+ );
+ if (!yes) {
+ return;
+ }
+ }
+
+ log.addComment(`Deleting ${webhooksToDelete.length} webhook/s.`);
+
+ const { failedWebhooks } = await processWebhooks(webhooksToDelete, log);
+
+ const failedWebhooksMessage = failedWebhooks.length
+ ? `with ${failedWebhooks.length} failed webhooks - check logs for details`
+ : ``;
+
+ log.appendLine(`Webhooks delete complete ${failedWebhooksMessage}`);
+
+ await log.close();
+};
diff --git a/src/commands/webhook/export.spec.ts b/src/commands/webhook/export.spec.ts
new file mode 100644
index 00000000..db2f4aed
--- /dev/null
+++ b/src/commands/webhook/export.spec.ts
@@ -0,0 +1,164 @@
+import Yargs from 'yargs/yargs';
+import * as exportWebhooksModule from './export';
+import { builder, command, handler, LOG_FILENAME } from './export';
+import dynamicContentClientFactory from '../../services/dynamic-content-client-factory';
+import { createLog, getDefaultLogPath } from '../../common/log-helpers';
+import readline from 'readline';
+import rmdir from 'rimraf';
+import { FileLog } from '../../common/file-log';
+import { Webhook } from 'dc-management-sdk-js';
+import MockPage from '../../common/dc-management-sdk-js/mock-page';
+import { existsSync } from 'fs';
+
+jest.mock('readline');
+jest.mock('../../services/dynamic-content-client-factory');
+jest.mock('../../common/log-helpers');
+
+function rimraf(dir: string): Promise {
+ return new Promise((resolve): void => {
+ rmdir(dir, resolve);
+ });
+}
+
+describe('webhook export command', () => {
+ afterEach((): void => {
+ jest.restoreAllMocks();
+ });
+
+ it('should command should defined', function () {
+ expect(command).toEqual('export ');
+ });
+
+ it('should use getDefaultLogPath for LOG_FILENAME with process.platform as default', function () {
+ LOG_FILENAME();
+
+ expect(getDefaultLogPath).toHaveBeenCalledWith('webhook', 'export', process.platform);
+ });
+
+ describe('builder tests', function () {
+ it('should configure yargs', function () {
+ 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', {
+ type: 'string',
+ describe:
+ 'The id of a the webhook to be exported. If id is not provided, this command will export ALL webhooks in the hub.'
+ });
+
+ expect(spyPositional).toHaveBeenCalledWith('dir', {
+ describe: 'Output directory for the exported webhooks',
+ type: 'string',
+ requiresArg: true
+ });
+
+ expect(spyOption).toHaveBeenCalledWith('logFile', {
+ type: 'string',
+ default: LOG_FILENAME,
+ describe: 'Path to a log file to write to.',
+ coerce: createLog
+ });
+ });
+ });
+
+ describe('handler tests', function () {
+ const yargArgs = {
+ $0: 'test',
+ _: ['test'],
+ json: true
+ };
+ const config = {
+ clientId: 'client-id',
+ clientSecret: 'client-id',
+ hubId: 'hub-id',
+ logFile: new FileLog()
+ };
+
+ const webhooksToExport: Webhook[] = [
+ new Webhook({
+ id: '1',
+ label: 'WH1',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ secret: 'xxxx',
+ method: 'POST'
+ }),
+ new Webhook({
+ id: '2',
+ label: 'WH2',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ secret: 'xxxx',
+ method: 'POST'
+ })
+ ];
+
+ beforeAll(async () => {
+ await rimraf(`temp_${process.env.JEST_WORKER_ID}/export/`);
+ });
+
+ let mockGetHub: jest.Mock;
+ let mockList: jest.Mock;
+
+ beforeEach((): void => {
+ const listResponse = new MockPage(Webhook, webhooksToExport);
+ mockList = jest.fn().mockResolvedValue(listResponse);
+
+ mockGetHub = jest.fn().mockResolvedValue({
+ related: {
+ webhooks: {
+ list: mockList
+ }
+ }
+ });
+
+ (dynamicContentClientFactory as jest.Mock).mockReturnValue({
+ hubs: {
+ get: mockGetHub
+ }
+ });
+ });
+
+ it('should export all webhooks to specified directory', async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (readline as any).setResponses(['y']);
+
+ const id: string[] | undefined = undefined;
+
+ const argv = {
+ ...yargArgs,
+ ...config,
+ id,
+ logFile: new FileLog(),
+ dir: `temp_${process.env.JEST_WORKER_ID}/export/`
+ };
+
+ jest.spyOn(exportWebhooksModule, 'handler');
+ await handler(argv);
+
+ expect(mockGetHub).toHaveBeenCalledWith('hub-id');
+ expect(mockList).toHaveBeenCalledTimes(1);
+ expect(mockList).toHaveBeenCalledWith({ size: 100 });
+
+ const spy = jest.spyOn(exportWebhooksModule, 'exportWebhooks');
+ await exportWebhooksModule.exportWebhooks(webhooksToExport, argv.dir, argv.logFile);
+
+ expect(spy).toHaveBeenCalledWith(webhooksToExport, argv.dir, argv.logFile);
+
+ webhooksToExport.forEach(webhook => {
+ const path = `temp_${process.env.JEST_WORKER_ID}/export/exported_webhooks/${webhook.label}.json`;
+
+ expect(existsSync(path)).toBe(true);
+ });
+
+ await rimraf(`temp_${process.env.JEST_WORKER_ID}/export/exported_webhooks/`);
+
+ spy.mockRestore();
+ });
+ });
+});
diff --git a/src/commands/webhook/export.ts b/src/commands/webhook/export.ts
new file mode 100644
index 00000000..781d6918
--- /dev/null
+++ b/src/commands/webhook/export.ts
@@ -0,0 +1,125 @@
+import { Arguments, Argv } from 'yargs';
+import { ConfigurationParameters } from '../configure';
+import dynamicContentClientFactory from '../../services/dynamic-content-client-factory';
+import { createLog, getDefaultLogPath } from '../../common/log-helpers';
+import { nothingExportedExit, uniqueFilenamePath, writeJsonToFile } from '../../services/export.service';
+import { Webhook } from 'dc-management-sdk-js';
+import { FileLog } from '../../common/file-log';
+import { join } from 'path';
+import sanitize from 'sanitize-filename';
+import { ensureDirectoryExists } from '../../common/import/directory-utils';
+import { progressBar } from '../../common/progress-bar/progress-bar';
+import { confirmAllContent } from '../../common/content-item/confirm-all-content';
+import { getWebhooksByIds } from '../../common/webhooks/get-webhooks-by-ids';
+import { getAllWebhooks } from '../../common/webhooks/get-all-webhook';
+import { ExportWebhookBuilderOptions } from '../../interfaces/export-webhook-builder-options';
+
+export const command = 'export ';
+
+export const desc = 'Export Webhooks';
+
+export const LOG_FILENAME = (platform: string = process.platform): string =>
+ getDefaultLogPath('webhook', 'export', platform);
+
+export const builder = (yargs: Argv): void => {
+ yargs
+ .positional('id', {
+ type: 'string',
+ describe:
+ 'The id of a the webhook to be exported. If id is not provided, this command will export ALL webhooks in the hub.'
+ })
+ .positional('dir', {
+ describe: 'Output directory for the exported webhooks',
+ type: 'string',
+ requiresArg: true
+ })
+ .option('logFile', {
+ type: 'string',
+ default: LOG_FILENAME,
+ describe: 'Path to a log file to write to.',
+ coerce: createLog
+ })
+ .alias('f', 'force')
+ .option('f', {
+ type: 'boolean',
+ boolean: true,
+ describe: 'If present, there will be no confirmation prompt before exporting the webhooks.'
+ })
+ .alias('s', 'silent')
+ .option('s', {
+ type: 'boolean',
+ boolean: true,
+ describe: 'If present, no log file will be produced.'
+ });
+};
+
+export const exportWebhooks = async (webhooks: Webhook[], dir: string, log: FileLog): Promise => {
+ const progress = progressBar(webhooks.length, 0, {
+ title: `Exporting ${webhooks.length} webhooks.`
+ });
+
+ const filenames: string[] = [];
+
+ for (let i = 0; i < webhooks.length; i++) {
+ const webhook = webhooks[i];
+
+ try {
+ let resolvedPath: string;
+ resolvedPath = 'exported_webhooks';
+
+ const directory = join(dir, resolvedPath);
+ resolvedPath = uniqueFilenamePath(directory, `${sanitize(webhook.label as string)}`, 'json', filenames);
+ filenames.push(resolvedPath);
+
+ await ensureDirectoryExists(directory);
+
+ writeJsonToFile(resolvedPath, webhook);
+ log.addComment(`Successfully exported "${webhook.label}"`);
+ progress.increment();
+ } catch (e) {
+ log.addComment(`Failed to export ${webhook.label}: ${e.toString()}`);
+ progress.increment();
+ }
+ }
+
+ progress.stop();
+};
+
+export const handler = async (
+ argv: Arguments
+): Promise => {
+ const { id, logFile, dir, force, silent } = argv;
+ const log = logFile.open();
+ const client = dynamicContentClientFactory(argv);
+ const allWebhooks = !id;
+ const hub = await client.hubs.get(argv.hubId);
+ let ids: string[] = [];
+
+ if (id) {
+ ids = Array.isArray(id) ? id : [id];
+ }
+
+ const webhooksToExport = ids.length > 0 ? await getWebhooksByIds(hub, ids) : await getAllWebhooks(hub);
+
+ if (!webhooksToExport.length) {
+ nothingExportedExit(log, 'No webhooks to export from this hub, exiting.');
+ return;
+ }
+
+ log.appendLine(`Found ${webhooksToExport.length} webhooks to export`);
+
+ if (!force) {
+ const yes = await confirmAllContent('export', 'webhooks', allWebhooks, false);
+ if (!yes) {
+ return;
+ }
+ }
+
+ log.appendLine(`Exporting ${webhooksToExport.length} webhooks.`);
+
+ await exportWebhooks(webhooksToExport, dir, log);
+
+ log.appendLine(`Finished successfully exporting ${webhooksToExport.length} webhooks`);
+
+ await log.close(!silent);
+};
diff --git a/src/commands/webhook/import.spec.ts b/src/commands/webhook/import.spec.ts
new file mode 100644
index 00000000..9aaca261
--- /dev/null
+++ b/src/commands/webhook/import.spec.ts
@@ -0,0 +1,231 @@
+import { builder, command, getDefaultMappingPath, handler, LOG_FILENAME } from './import';
+import * as importModule from './import';
+import { Hub, Webhook } from 'dc-management-sdk-js';
+import Yargs from 'yargs/yargs';
+import rmdir from 'rimraf';
+import { FileLog } from '../../common/file-log';
+import { ContentMapping } from '../../common/content-mapping';
+import { mockValues } from './webhook-test-helpers';
+import { createLog, getDefaultLogPath } from '../../common/log-helpers';
+import { ensureDirectoryExists } from '../../common/import/directory-utils';
+import { exportWebhooks } from './export';
+import readline from 'readline';
+
+jest.mock('../../services/dynamic-content-client-factory');
+jest.mock('../../services/import.service');
+jest.mock('readline');
+
+jest.mock('../../common/log-helpers', () => ({
+ ...jest.requireActual('../../common/log-helpers'),
+ getDefaultLogPath: jest.fn()
+}));
+
+function rimraf(dir: string): Promise {
+ return new Promise((resolve): void => {
+ rmdir(dir, resolve);
+ });
+}
+
+describe('webhook import command', () => {
+ afterEach((): void => {
+ jest.restoreAllMocks();
+ });
+
+ it('should command should defined', function () {
+ expect(command).toEqual('import ');
+ });
+
+ it('should use getDefaultLogPath for LOG_FILENAME with process.platform as default', function () {
+ LOG_FILENAME();
+
+ expect(getDefaultLogPath).toHaveBeenCalledWith('webhook', 'import', process.platform);
+ });
+
+ it('should generate a default mapping path containing the given name', function () {
+ expect(getDefaultMappingPath('hub-1').indexOf('hub-1')).not.toEqual(-1);
+ });
+
+ describe('builder tests', function () {
+ it('should configure yargs', function () {
+ 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('dir', {
+ describe: 'Directory containing webhooks to import.',
+ type: 'string',
+ requiresArg: true
+ });
+
+ expect(spyOption).toHaveBeenCalledWith('mapFile', {
+ type: 'string',
+ describe:
+ 'Mapping file to use when updating webhooks that already exists. Updated with any new mappings that are generated. If not present, will be created.'
+ });
+
+ expect(spyOption).toHaveBeenCalledWith('logFile', {
+ type: 'string',
+ default: LOG_FILENAME,
+ describe: 'Path to a log file to write to.',
+ coerce: createLog
+ });
+
+ expect(spyOption).toHaveBeenCalledWith('f', {
+ type: 'boolean',
+ boolean: true,
+ describe: 'Overwrite webhooks.'
+ });
+
+ expect(spyOption).toHaveBeenCalledWith('s', {
+ type: 'boolean',
+ boolean: true,
+ describe: 'If present, no log file will be produced.'
+ });
+ });
+ });
+
+ describe('handler tests', function () {
+ beforeAll(async () => {
+ await rimraf(`temp_${process.env.JEST_WORKER_ID}/importWebhook/`);
+ });
+
+ afterAll(async () => {
+ await rimraf(`temp_${process.env.JEST_WORKER_ID}/importWebhook/`);
+ });
+
+ it('should call importWebhooks with the loaded webhook, then save the mapping', async function () {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (readline as any).setResponses(['y']);
+
+ const { getHubMock } = mockValues({});
+ const logFile = new FileLog();
+
+ const yargArgs = {
+ $0: 'test',
+ _: ['test'],
+ json: true
+ };
+ const config = {
+ clientId: 'client-id',
+ clientSecret: 'client-id',
+ hubId: 'hub-1',
+ logFile
+ };
+
+ const webhookObj = {
+ id: '1',
+ label: 'WH1',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ method: 'POST'
+ };
+
+ const webhooks = [new Webhook(webhookObj)];
+
+ await exportWebhooks(webhooks, `temp_${process.env.JEST_WORKER_ID}/importWebhook/`, logFile);
+
+ const importWebhooks = jest.spyOn(importModule, 'importWebhooks').mockResolvedValue();
+ const trySaveMapping = jest.spyOn(importModule, 'trySaveMapping').mockResolvedValue();
+
+ const getDefaultMappingPathSpy = jest.spyOn(importModule, 'getDefaultMappingPath');
+ const mappingPath = importModule.getDefaultMappingPath('hub-1');
+
+ const argv = {
+ ...yargArgs,
+ ...config,
+ dir: `temp_${process.env.JEST_WORKER_ID}/importWebhook/exported_webhooks`
+ };
+
+ await handler(argv);
+
+ expect(getHubMock).toHaveBeenCalledWith('hub-1');
+
+ expect(importWebhooks).toHaveBeenCalledWith(
+ expect.any(Hub),
+ expect.arrayContaining([expect.objectContaining(webhookObj)]),
+ expect.any(ContentMapping),
+ logFile
+ );
+
+ expect(getDefaultMappingPathSpy).toHaveBeenCalledWith('hub-1');
+
+ expect(trySaveMapping).toHaveBeenCalledWith(
+ expect.stringContaining('hub-1.json'),
+ expect.any(ContentMapping),
+ logFile
+ );
+ expect(logFile.closed).toBeTruthy();
+
+ rimraf(mappingPath);
+ });
+
+ it('should load an existing mapping file', async function () {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (readline as any).setResponses(['y']);
+
+ const { getHubMock } = mockValues({});
+ const logFile = new FileLog();
+
+ const yargArgs = {
+ $0: 'test',
+ _: ['test'],
+ json: true
+ };
+ const config = {
+ clientId: 'client-id',
+ clientSecret: 'client-id',
+ hubId: 'hub-1',
+ logFile
+ };
+
+ const webhookObj = {
+ id: '1',
+ label: 'WH1',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ method: 'POST'
+ };
+
+ const webhooks = [new Webhook(webhookObj)];
+
+ await exportWebhooks(webhooks, `temp_${process.env.JEST_WORKER_ID}/importWebhook/`, logFile);
+
+ const argv = {
+ ...yargArgs,
+ ...config,
+ logFile,
+ mapFile: `temp_${process.env.JEST_WORKER_ID}/importWebhook/hub-1(existing-mapping).json`,
+ dir: `temp_${process.env.JEST_WORKER_ID}/importWebhook/exported_webhooks`
+ };
+
+ const importWebhooks = jest.spyOn(importModule, 'importWebhooks').mockResolvedValue();
+ const trySaveMapping = jest.spyOn(importModule, 'trySaveMapping').mockResolvedValue();
+
+ const getDefaultMappingPathSpy = jest.spyOn(importModule, 'getDefaultMappingPath');
+
+ await ensureDirectoryExists(`temp_${process.env.JEST_WORKER_ID}/importWebhook/`);
+
+ const existingMapping = new ContentMapping();
+ await existingMapping.save(argv.mapFile);
+
+ await handler(argv);
+
+ expect(getHubMock).toHaveBeenCalledWith('hub-1');
+
+ expect(importWebhooks).toHaveBeenCalledWith(
+ expect.any(Hub),
+ expect.arrayContaining([expect.objectContaining(webhookObj)]),
+ expect.any(ContentMapping),
+ logFile
+ );
+
+ expect(getDefaultMappingPathSpy).not.toHaveBeenCalled();
+ expect(trySaveMapping).toHaveBeenCalledWith(argv.mapFile, expect.any(ContentMapping), logFile);
+ expect(logFile.closed).toBeTruthy();
+ });
+ });
+});
diff --git a/src/commands/webhook/import.ts b/src/commands/webhook/import.ts
new file mode 100644
index 00000000..11cdeda5
--- /dev/null
+++ b/src/commands/webhook/import.ts
@@ -0,0 +1,267 @@
+import { Arguments, Argv } from 'yargs';
+import { ConfigurationParameters } from '../configure';
+import dynamicContentClientFactory from '../../services/dynamic-content-client-factory';
+import { FileLog } from '../../common/file-log';
+import { join, extname } from 'path';
+import { readdir, readFile } from 'graceful-fs';
+import { promisify } from 'util';
+import { Hub, Webhook } from 'dc-management-sdk-js';
+import { ContentMapping } from '../../common/content-mapping';
+import { createLog, getDefaultLogPath } from '../../common/log-helpers';
+import { asyncQuestion } from '../../common/question-helpers';
+import { progressBar } from '../../common/progress-bar/progress-bar';
+import PublishOptions from '../../common/publish/publish-options';
+import { ImportWebhookBuilderOptions } from '../../interfaces/import-webhook-builder-options';
+
+export function getDefaultMappingPath(name: string, platform: string = process.platform): string {
+ return join(
+ process.env[platform == 'win32' ? 'USERPROFILE' : 'HOME'] || __dirname,
+ '.amplience',
+ `imports/`,
+ `${name}.json`
+ );
+}
+
+export const command = 'import ';
+
+export const desc = 'Import Webhooks';
+
+export const LOG_FILENAME = (platform: string = process.platform): string =>
+ getDefaultLogPath('webhook', 'import', platform);
+
+export const builder = (yargs: Argv): void => {
+ yargs
+ .positional('dir', {
+ describe: 'Directory containing webhooks to import.',
+ type: 'string',
+ requiresArg: true
+ })
+ .option('mapFile', {
+ type: 'string',
+ describe:
+ 'Mapping file to use when updating webhooks that already exists. Updated with any new mappings that are generated. If not present, will be created.'
+ })
+ .option('logFile', {
+ type: 'string',
+ default: LOG_FILENAME,
+ describe: 'Path to a log file to write to.',
+ coerce: createLog
+ })
+ .alias('f', 'force')
+ .option('f', {
+ type: 'boolean',
+ boolean: true,
+ describe: 'Overwrite webhooks.'
+ })
+ .alias('s', 'silent')
+ .option('s', {
+ type: 'boolean',
+ boolean: true,
+ describe: 'If present, no log file will be produced.'
+ });
+};
+
+interface WebhookImportResult {
+ webhook: Webhook;
+ state: 'UPDATED' | 'CREATED';
+}
+
+export const createOrUpdateWebhook = async (
+ hub: Hub,
+ item: Webhook,
+ existing: string | Webhook | null
+): Promise => {
+ let oldItem: Webhook | null = null;
+ if (typeof existing === 'string') {
+ oldItem = await hub.related.webhooks.get(existing);
+ } else {
+ oldItem = existing;
+ }
+
+ let result: WebhookImportResult;
+
+ if (oldItem == null) {
+ result = { webhook: await hub.related.webhooks.create(item), state: 'CREATED' };
+ } else {
+ result = { webhook: await oldItem.related.update(item), state: 'UPDATED' };
+ }
+
+ return result;
+};
+
+export const trySaveMapping = async (
+ mapFile: string | undefined,
+ mapping: ContentMapping,
+ log: FileLog
+): Promise => {
+ if (mapFile != null) {
+ try {
+ await mapping.save(mapFile);
+ } catch (e) {
+ log.appendLine(`Failed to save the mapping. ${e.toString()}`);
+ }
+ }
+};
+
+export const prepareWebhooksForImport = async (
+ hub: Hub,
+ webhookFiles: string[],
+ mapping: ContentMapping,
+ log: FileLog,
+ argv: Arguments
+): Promise => {
+ const { force } = argv;
+
+ let webhooks: Webhook[] = [];
+
+ for (const webhookFile of webhookFiles) {
+ log.appendLine(`Reading webhook data in '${webhookFile}' for hub '${hub.label}'...`);
+
+ if (extname(webhookFile) !== '.json') {
+ return null;
+ }
+
+ let webhookJSON;
+
+ try {
+ const webhookText = await promisify(readFile)(webhookFile, {
+ encoding: 'utf8'
+ });
+
+ webhookJSON = JSON.parse(webhookText);
+ } catch (e) {
+ log.appendLine(`Couldn't read webhook at '${webhookFile}': ${e.toString()}`);
+ return null;
+ }
+
+ const webhook = new Webhook({
+ ...(webhookJSON.id && { id: webhookJSON.id }),
+ ...(webhookJSON.label && { label: webhookJSON.label }),
+ ...(webhookJSON.events && { events: webhookJSON.events }),
+ ...(webhookJSON.active && { active: webhookJSON.active }),
+ ...(webhookJSON.handlers && { handlers: webhookJSON.handlers }),
+ ...(webhookJSON.notifications && { notifications: webhookJSON.notifications }),
+ ...(webhookJSON.headers && { headers: webhookJSON.headers }),
+ ...(webhookJSON.filters && { filters: webhookJSON.filters }),
+ ...(webhookJSON.customPayload && { customPayload: webhookJSON.customPayload }),
+ ...(webhookJSON.method && { method: webhookJSON.method })
+ });
+
+ if (webhook?.headers) {
+ webhook.headers = webhook.headers.filter(header => {
+ return !header.secret;
+ });
+ }
+
+ if (webhook?.customPayload?.value) {
+ webhook.customPayload.value = webhook.customPayload.value
+ .replace(/account="([^"]*)"/g, `account="${hub.name}"`)
+ .replace(
+ /stagingEnvironment="([^"]*)"/g,
+ `stagingEnvironment="${hub.settings?.virtualStagingEnvironment?.hostname}"`
+ );
+ }
+
+ webhooks.push(new Webhook(webhook));
+ }
+
+ const alreadyExists = webhooks.filter(item => mapping.getWebhook(item.id) != null);
+ if (alreadyExists.length > 0) {
+ const updateExisting =
+ force ||
+ (await asyncQuestion(
+ `${alreadyExists.length} of the webhooks being imported already exist in the mapping. Would you like to update these webhooks instead of skipping them? (y/n) `,
+ log
+ ));
+
+ if (!updateExisting) {
+ webhooks = webhooks.filter(item => mapping.getWebhook(item.id) == null);
+ }
+ }
+
+ return webhooks;
+};
+
+export const importWebhooks = async (
+ hub: Hub,
+ webhooks: Webhook[],
+ mapping: ContentMapping,
+ log: FileLog
+): Promise => {
+ const importProgress = progressBar(webhooks.length, 0, {
+ title: 'Importing webhooks'
+ });
+
+ for (const webhookToImport of webhooks) {
+ const originalId = webhookToImport.id;
+ webhookToImport.id = mapping.getWebhook(webhookToImport.id as string) || '';
+
+ if (!webhookToImport.id) {
+ delete webhookToImport.id;
+ }
+
+ try {
+ const { webhook, state } = await createOrUpdateWebhook(
+ hub,
+ webhookToImport,
+ mapping.getWebhook(originalId as string) || null
+ );
+
+ log.addComment(`${state} ${webhook.label}.`);
+ log.addAction(state, (webhook.id || 'unknown') + (state === 'UPDATED' ? ` ${webhook.label}` : ''));
+
+ mapping.registerWebhook(originalId as string, webhook.id as string);
+
+ importProgress.increment();
+ } catch (e) {
+ importProgress.stop();
+ log.error(`Failed creating ${webhookToImport.label}:`, e);
+ throw Error(`Importing webhook failed. Error: ${e.toString()}`);
+ }
+ }
+
+ importProgress.stop();
+};
+
+export const handler = async (
+ argv: Arguments
+): Promise => {
+ const { dir, logFile, force, silent, mapFile: mapFileArg } = argv;
+ const client = dynamicContentClientFactory(argv);
+ const log = logFile.open();
+ const hub: Hub = await client.hubs.get(argv.hubId);
+ const importTitle = `hub-${hub.id}`;
+ const mapping = new ContentMapping();
+ const mapFile = mapFileArg ? mapFileArg : getDefaultMappingPath(importTitle);
+
+ if (await mapping.load(mapFile)) {
+ log.appendLine(`Existing mapping loaded from '${mapFile}', changes will be saved back to it.`);
+ } else {
+ log.appendLine(`Creating new mapping file at '${mapFile}'.`);
+ }
+
+ const baseDirContents = await promisify(readdir)(dir);
+
+ const webhookFiles = baseDirContents.map(wh => {
+ return `${dir}/${wh}`;
+ });
+
+ const webhooks = await prepareWebhooksForImport(hub, webhookFiles, mapping, log, argv);
+
+ if (webhooks !== null) {
+ const proceedImport =
+ force ||
+ (await asyncQuestion(`${webhooks.length} webhook/s will be imported, do you wish to continue? (y/n) `, log));
+ if (!proceedImport) {
+ return;
+ }
+
+ await importWebhooks(hub, webhooks, mapping, log);
+ } else {
+ log.appendLine('No webhooks found to import.');
+ }
+
+ trySaveMapping(mapFile, mapping, log);
+
+ await log.close(!silent);
+};
diff --git a/src/commands/webhook/webhook-test-helpers.ts b/src/commands/webhook/webhook-test-helpers.ts
new file mode 100644
index 00000000..de9db462
--- /dev/null
+++ b/src/commands/webhook/webhook-test-helpers.ts
@@ -0,0 +1,103 @@
+import { Hub, Webhook } from 'dc-management-sdk-js';
+import MockPage from '../../common/dc-management-sdk-js/mock-page';
+import dynamicContentClientFactory from '../../services/dynamic-content-client-factory';
+
+export const mockValues = ({
+ getHubError = false,
+ getWebhookError = false,
+ listWebhookError = false
+}): {
+ mockGet: () => void;
+ getHubMock: () => void;
+ mockWebhooksList: () => void;
+ mockWebhookUpdate: () => void;
+ mockWebhookCreate: () => void;
+} => {
+ const mockGet = jest.fn();
+ const getHubMock = jest.fn();
+ const mockWebhooksList = jest.fn();
+ const mockWebhookUpdate = jest.fn();
+ const mockWebhookCreate = jest.fn();
+
+ (dynamicContentClientFactory as jest.Mock).mockReturnValue({
+ hubs: {
+ get: getHubMock
+ },
+ webhooks: {
+ get: mockGet
+ }
+ });
+
+ const hub = new Hub({
+ name: '1',
+ id: '1',
+ _links: {
+ webhooks: {
+ href: 'https://api.amplience.net/v2/content/webhooks',
+ templated: true
+ }
+ }
+ });
+
+ getHubMock.mockResolvedValue(hub);
+
+ hub.related.webhooks.list = mockWebhooksList;
+ hub.related.webhooks.create = mockWebhookCreate;
+
+ const webhooks = [
+ new Webhook({
+ id: '1',
+ label: 'WH1',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ secret: 'xxxx',
+ method: 'POST'
+ }),
+ new Webhook({
+ id: '2',
+ label: 'WH2',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ secret: 'xxxx',
+ method: 'POST'
+ })
+ ];
+
+ mockWebhooksList.mockResolvedValue(new MockPage(Webhook, webhooks));
+
+ const webhook = new Webhook({
+ id: '1',
+ label: 'WH1',
+ events: ['dynamic-content.content-item.updated'],
+ active: true,
+ handlers: ['https://test.this/webhook'],
+ secret: 'xxxx',
+ method: 'POST'
+ });
+
+ mockGet.mockResolvedValue(webhook);
+
+ webhook.related.update = mockWebhookUpdate;
+
+ if (getHubError) {
+ getHubMock.mockRejectedValue(new Error('Error'));
+ }
+
+ if (getWebhookError) {
+ mockGet.mockRejectedValue(new Error('Error'));
+ }
+
+ if (listWebhookError) {
+ mockWebhooksList.mockRejectedValue(new Error('Error'));
+ }
+
+ return {
+ mockGet,
+ getHubMock,
+ mockWebhooksList,
+ mockWebhookUpdate,
+ mockWebhookCreate
+ };
+};
diff --git a/src/common/content-mapping.ts b/src/common/content-mapping.ts
index 50e09711..b834c725 100644
--- a/src/common/content-mapping.ts
+++ b/src/common/content-mapping.ts
@@ -9,6 +9,7 @@ export class ContentMapping {
editions: Map;
slots: Map;
snapshots: Map;
+ webhooks: Map;
constructor() {
this.contentItems = new Map();
@@ -17,6 +18,7 @@ export class ContentMapping {
this.editions = new Map();
this.slots = new Map();
this.snapshots = new Map();
+ this.webhooks = new Map();
}
getContentItem(id: string | undefined): string | undefined {
@@ -71,6 +73,14 @@ export class ContentMapping {
this.snapshots.set(fromId, toId);
}
+ getWebhook(id: string | undefined): string | undefined {
+ return id === undefined ? undefined : this.webhooks.get(id);
+ }
+
+ registerWebhook(fromId: string, toId: string): void {
+ this.webhooks.set(fromId, toId);
+ }
+
async save(filename: string): Promise {
const obj: SerializedContentMapping = {
contentItems: Array.from(this.contentItems),
@@ -78,7 +88,8 @@ export class ContentMapping {
events: Array.from(this.events),
editions: Array.from(this.editions),
slots: Array.from(this.slots),
- snapshots: Array.from(this.snapshots)
+ snapshots: Array.from(this.snapshots),
+ webhooks: Array.from(this.webhooks)
};
const text = JSON.stringify(obj);
@@ -101,6 +112,7 @@ export class ContentMapping {
this.editions = obj.editions ? new Map(obj.editions) : new Map();
this.slots = obj.slots ? new Map(obj.slots) : new Map();
this.snapshots = obj.snapshots ? new Map(obj.snapshots) : new Map();
+ this.webhooks = obj.webhooks ? new Map(obj.webhooks) : new Map();
return true;
} catch (e) {
return false;
@@ -115,4 +127,5 @@ interface SerializedContentMapping {
editions?: [string, string][];
slots?: [string, string][];
snapshots?: [string, string][];
+ webhooks?: [string, string][];
}
diff --git a/src/common/webhooks/get-all-webhook.ts b/src/common/webhooks/get-all-webhook.ts
new file mode 100644
index 00000000..0a882817
--- /dev/null
+++ b/src/common/webhooks/get-all-webhook.ts
@@ -0,0 +1,6 @@
+import { Hub } from 'dc-management-sdk-js';
+import paginator from '../dc-management-sdk-js/paginator';
+
+export const getAllWebhooks = async (hub: Hub) => {
+ return await paginator(hub.related.webhooks.list);
+};
diff --git a/src/common/webhooks/get-webhooks-by-ids.ts b/src/common/webhooks/get-webhooks-by-ids.ts
new file mode 100644
index 00000000..0d268ffe
--- /dev/null
+++ b/src/common/webhooks/get-webhooks-by-ids.ts
@@ -0,0 +1,15 @@
+import { Hub } from 'dc-management-sdk-js';
+
+export const getWebhooksByIds = async (hub: Hub, ids: string[]) => {
+ const webhooks = [];
+
+ for (const id of ids) {
+ try {
+ webhooks.push(await hub.related.webhooks.get(id));
+ } catch (e) {
+ // silently fail missing webhooks
+ }
+ }
+
+ return webhooks;
+};
diff --git a/src/interfaces/delete-webhook-builder-options.ts b/src/interfaces/delete-webhook-builder-options.ts
new file mode 100644
index 00000000..9bdf3881
--- /dev/null
+++ b/src/interfaces/delete-webhook-builder-options.ts
@@ -0,0 +1,6 @@
+import { FileLog } from '../common/file-log';
+
+export interface DeleteWebhookBuilderOptions {
+ logFile: FileLog;
+ force?: boolean;
+}
diff --git a/src/interfaces/export-item-builder-options.interface.ts b/src/interfaces/export-item-builder-options.interface.ts
index bddae8e5..a6d767e7 100644
--- a/src/interfaces/export-item-builder-options.interface.ts
+++ b/src/interfaces/export-item-builder-options.interface.ts
@@ -7,6 +7,5 @@ export interface ExportItemBuilderOptions {
facet?: string;
logFile: FileLog;
publish?: boolean;
-
exportedIds?: string[];
}
diff --git a/src/interfaces/export-webhook-builder-options.ts b/src/interfaces/export-webhook-builder-options.ts
new file mode 100644
index 00000000..3e130237
--- /dev/null
+++ b/src/interfaces/export-webhook-builder-options.ts
@@ -0,0 +1,9 @@
+import { FileLog } from '../common/file-log';
+
+export interface ExportWebhookBuilderOptions {
+ dir: string;
+ id?: string | string[];
+ logFile: FileLog;
+ force?: boolean;
+ silent?: boolean;
+}
diff --git a/src/interfaces/import-webhook-builder-options.ts b/src/interfaces/import-webhook-builder-options.ts
new file mode 100644
index 00000000..b8bf0db3
--- /dev/null
+++ b/src/interfaces/import-webhook-builder-options.ts
@@ -0,0 +1,9 @@
+import { FileLog } from '../common/file-log';
+
+export interface ImportWebhookBuilderOptions {
+ dir: string;
+ logFile: FileLog;
+ force?: boolean;
+ silent?: boolean;
+ mapFile?: string;
+}