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; +}