diff --git a/src/commands/content-item/archive.spec.ts b/src/commands/content-item/archive.spec.ts index 39280974..9610f0e2 100644 --- a/src/commands/content-item/archive.spec.ts +++ b/src/commands/content-item/archive.spec.ts @@ -1,4 +1,4 @@ -import { builder, command, handler, LOG_FILENAME, getContentItems, processItems, coerceLog } from './archive'; +import { builder, command, handler, LOG_FILENAME, coerceLog } from './archive'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { ContentRepository, ContentItem, Folder, Status } from 'dc-management-sdk-js'; import Yargs from 'yargs/yargs'; @@ -6,7 +6,7 @@ import readline from 'readline'; import MockPage from '../../common/dc-management-sdk-js/mock-page'; import { dirname } from 'path'; import { promisify } from 'util'; -import { exists, readFile, unlink, mkdir, writeFile } from 'fs'; +import { readFile, unlink, mkdir, writeFile, existsSync } from 'fs'; import { FileLog, setVersion } from '../../common/file-log'; import { createLog, getDefaultLogPath } from '../../common/log-helpers'; import * as fetchContentModule from '../../common/filter/fetch-content'; @@ -51,7 +51,7 @@ describe('content-item archive command', () => { mockItemUpdate: () => void; mockRepoGet: () => void; mockFolderGet: () => void; - mockFacet: () => void; + mockGetContent: () => void; contentItems: ContentItem[]; } => { const mockGet = jest.fn(); @@ -62,7 +62,7 @@ describe('content-item archive command', () => { const mockItemUpdate = jest.fn(); const mockRepoGet = jest.fn(); const mockFolderGet = jest.fn(); - const mockFacet = jest.spyOn(fetchContentModule, 'getContent') as jest.Mock; + const mockGetContent = jest.spyOn(fetchContentModule, 'getContent') as jest.Mock; const contentItems = [ new ContentItem({ @@ -209,7 +209,7 @@ describe('content-item archive command', () => { mockItemsList.mockResolvedValue(new MockPage(ContentItem, contentItems)); - mockFacet.mockResolvedValue(contentItems); + mockGetContent.mockResolvedValue(contentItems); if (archiveError) { mockArchive.mockRejectedValue(new Error('Error')); @@ -226,7 +226,7 @@ describe('content-item archive command', () => { mockItemUpdate, mockRepoGet, mockFolderGet, - mockFacet, + mockGetContent, contentItems }; }; @@ -329,7 +329,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockFacet, mockArchive } = mockValues(); + const { mockGet, mockGetContent, mockArchive } = mockValues(); const argv = { ...yargArgs, @@ -338,7 +338,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, enrichItems: true }); @@ -349,7 +349,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockArchive, mockItemGetById, mockFacet } = mockValues(); + const { mockArchive, mockItemGetById, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -360,7 +360,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockItemGetById).toHaveBeenCalled(); - expect(mockFacet).not.toHaveBeenCalled(); + expect(mockGetContent).not.toHaveBeenCalled(); expect(mockArchive).toHaveBeenCalledTimes(1); }); @@ -368,7 +368,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockArchive, mockItemGetById, mockFacet } = mockValues(true); + const { mockArchive, mockItemGetById, mockGetContent } = mockValues(true); const argv = { ...yargArgs, @@ -378,7 +378,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockItemGetById).toHaveBeenCalled(); - expect(mockFacet).not.toHaveBeenCalled(); + expect(mockGetContent).not.toHaveBeenCalled(); expect(mockArchive).not.toHaveBeenCalled(); }); @@ -386,7 +386,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -396,7 +396,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, repoId: 'repo1', enrichItems: true @@ -408,7 +408,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -418,7 +418,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, repoId: ['repo1', 'repo2'], enrichItems: true @@ -430,7 +430,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -441,7 +441,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, folderId: 'folder1', repoId: 'repo123', @@ -454,7 +454,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -464,7 +464,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, folderId: ['folder1', 'folder1'], enrichItems: true @@ -476,7 +476,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -487,7 +487,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { folderId: 'folder1', status: Status.ACTIVE, enrichItems: true @@ -499,7 +499,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockArchive, mockFolderGet, mockItemsList, mockFacet } = mockValues(); + const { mockArchive, mockFolderGet, mockItemsList, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -509,7 +509,7 @@ describe('content-item archive command', () => { }; await handler(argv); - expect(mockFacet).not.toHaveBeenCalled(); + expect(mockGetContent).not.toHaveBeenCalled(); expect(mockFolderGet).not.toHaveBeenCalled(); expect(mockItemsList).not.toHaveBeenCalled(); expect(mockArchive).not.toHaveBeenCalled(); @@ -519,10 +519,10 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); - (mockFacet as jest.Mock).mockReset(); - (mockFacet as jest.Mock).mockResolvedValue([]); + (mockGetContent as jest.Mock).mockReset(); + (mockGetContent as jest.Mock).mockResolvedValue([]); const argv = { ...yargArgs, @@ -533,7 +533,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item3', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item3', { folderId: 'folder1', status: Status.ACTIVE, enrichItems: true @@ -545,7 +545,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['n']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -556,7 +556,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { folderId: 'folder1', status: Status.ACTIVE, enrichItems: true @@ -568,7 +568,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -578,7 +578,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'schema:http://test.com', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'schema:http://test.com', { status: Status.ACTIVE, enrichItems: true }); @@ -589,7 +589,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(true); + const { mockGet, mockArchive, mockGetContent } = mockValues(true); const argv = { ...yargArgs, @@ -599,7 +599,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, enrichItems: true }); @@ -610,7 +610,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockArchive, mockFacet } = mockValues(true); + const { mockGet, mockArchive, mockGetContent } = mockValues(true); const argv = { ...yargArgs, @@ -620,7 +620,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, enrichItems: true }); @@ -631,7 +631,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['input', 'ignored']); - const { mockGet, mockArchive, mockFacet } = mockValues(); + const { mockGet, mockArchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -641,7 +641,7 @@ describe('content-item archive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ACTIVE, enrichItems: true }); @@ -656,7 +656,7 @@ describe('content-item archive command', () => { const log = '// Type log test file\n' + 'UNARCHIVE 1\n' + 'UNARCHIVE 2\n' + 'UNARCHIVE idMissing\n'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); @@ -683,23 +683,24 @@ describe('content-item archive command', () => { expect(mockArchive).toHaveBeenCalledTimes(2); }); - it("shouldn't archive content items, getFacet error", async () => { + it('should not archive content items when getContent throws an error', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['input', 'ignored']); - const { mockArchive, mockFacet } = mockValues(true); + const { mockArchive, mockGetContent } = mockValues(true); - (mockFacet as jest.Mock).mockReset(); - (mockFacet as jest.Mock).mockRejectedValue(new Error('Simulated Error')); + (mockGetContent as jest.Mock).mockReset(); + (mockGetContent as jest.Mock).mockRejectedValue(new Error('Simulated error')); const argv = { ...yargArgs, ...config, folderId: 'folder1' }; - await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + await expect(handler(argv)).rejects.toThrowErrorMatchingInlineSnapshot(`"Simulated error"`); + + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { folderId: 'folder1', status: Status.ACTIVE, enrichItems: true @@ -707,11 +708,11 @@ describe('content-item archive command', () => { expect(mockArchive).not.toHaveBeenCalled(); }); - it("shouldn't archive content items, revertLog error", async () => { + it('should not archive content items when revertLog does not exist', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - if (await promisify(exists)(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`)) { + if (existsSync(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`)) { await promisify(unlink)(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`); } @@ -719,12 +720,12 @@ describe('content-item archive command', () => { const log = '// Type log test file\n' + 'UNARCHIVE 1\n' + 'UNARCHIVE 2\n' + 'UNARCHIVE idMissing'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); - const { mockArchive, mockItemGetById, mockFacet } = mockValues(true); + const { mockArchive, mockItemGetById, mockGetContent } = mockValues(true); const argv = { ...yargArgs, @@ -733,10 +734,13 @@ describe('content-item archive command', () => { force: true, revertLog: 'wrongFileName.log' }; - await handler(argv); + + await expect(handler(argv)).rejects.toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open 'wrongFileName.log'"` + ); expect(mockItemGetById).not.toHaveBeenCalled(); - expect(mockFacet).not.toHaveBeenCalled(); + expect(mockGetContent).not.toHaveBeenCalled(); expect(mockArchive).not.toHaveBeenCalled(); }); @@ -744,7 +748,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - if (await promisify(exists)(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`)) { + if (existsSync(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`)) { await promisify(unlink)(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`); } @@ -764,7 +768,7 @@ describe('content-item archive command', () => { expect(mockItemGetById).toHaveBeenCalled(); expect(mockArchive).toHaveBeenCalled(); - const logExists = await promisify(exists)(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`); + const logExists = existsSync(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`); expect(logExists).toBeTruthy(); @@ -842,104 +846,31 @@ describe('content-item archive command', () => { expect(mockItemUpdate).toHaveBeenCalledTimes(1); expect((mockItemUpdate as jest.Mock).mock.calls[0][1].ignoreSchemaValidation).toBe(true); }); - }); - describe('getContentItems tests', () => { - it('should get content items by id', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - id: '1', - hubId: 'hub1' - }); - - if (result) { - expect(result.contentItems.length).toBeGreaterThanOrEqual(1); - - expect(result.contentItems[0].id).toMatch('1'); - } - }); - - it('should get content items all', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - hubId: 'hub1' - }); - - if (result) { - expect(result.contentItems.length).toBe(2); - } - }); - - it('should get content items by repo', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - hubId: 'hub1', - repoId: 'repo1' - }); - - if (result) { - expect(result.contentItems.length).toBe(2); - } - }); - - it('should get content items by folder', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - hubId: 'hub1', - folderId: 'folder1' - }); - - if (result) { - expect(result.contentItems.length).toBe(2); - } - }); - }); - - describe('processItems tests', () => { - it('should archive content items', async () => { - const { contentItems, mockArchive } = mockValues(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (readline as any).setResponses(['y']); - - await processItems({ - contentItems, - allContent: true, - missingContent: false, - logFile: createLog('./logFile.log') + it('should not archive content items', async () => { + const { mockGet, mockItemGetById, mockArchive } = mockValues(); + const logFile = new FileLog(); + const mockAppendFile = jest.fn(); + logFile.open = jest.fn().mockImplementation(() => { + return { + appendLine: mockAppendFile + }; }); + const argv = { + ...yargArgs, + ...config, + id: 'repo123', + logFile + }; - expect(mockArchive).toHaveBeenCalledTimes(2); - - if (await promisify(exists)('./logFile.log')) { - await promisify(unlink)('./logFile.log'); - } - }); - - it('should not archive content items', async () => { - jest.spyOn(global.console, 'log'); + (mockItemGetById as jest.Mock).mockResolvedValue([]); - await processItems({ - contentItems: [], - allContent: true, - missingContent: false, - logFile: new FileLog() - }); + await handler(argv); - expect(console.log).toHaveBeenCalled(); - expect(console.log).toHaveBeenLastCalledWith('Nothing found to archive, aborting.'); + expect(mockGet).toHaveBeenCalled(); + expect(mockArchive).not.toHaveBeenCalled(); + expect(mockAppendFile).toHaveBeenCalled(); + expect(mockAppendFile).toHaveBeenLastCalledWith('Nothing found to archive, aborting'); }); }); }); diff --git a/src/commands/content-item/archive.ts b/src/commands/content-item/archive.ts index a14bb4fa..891c5aa9 100644 --- a/src/commands/content-item/archive.ts +++ b/src/commands/content-item/archive.ts @@ -4,12 +4,13 @@ import dynamicContentClientFactory from '../../services/dynamic-content-client-f import { ArchiveLog } from '../../common/archive/archive-log'; import { confirmAllContent } from '../../common/content-item/confirm-all-content'; import ArchiveOptions from '../../common/archive/archive-options'; -import { ContentItem, DynamicContent, Status } from 'dc-management-sdk-js'; +import { ContentItem, Status } from 'dc-management-sdk-js'; import { getDefaultLogPath, createLog } from '../../common/log-helpers'; import { FileLog } from '../../common/file-log'; import { withOldFilters } from '../../common/filter/facet'; import { getContent } from '../../common/filter/fetch-content'; import { progressBar } from '../../common/progress-bar/progress-bar'; +import { getContentByIds } from '../../common/content-item/get-content-items-by-ids'; export const command = 'archive [id]'; @@ -86,108 +87,20 @@ export const builder = (yargs: Argv): void => { }); }; -export const getContentItems = async ({ - client, - id, - hubId, - repoId, - folderId, - revertLog, - facet -}: { - client: DynamicContent; - id?: string | string[]; - hubId: string; - repoId?: string | string[]; - folderId?: string | string[]; - revertLog?: string; - facet?: string; -}): Promise<{ contentItems: ContentItem[]; missingContent: boolean }> => { - try { - let contentItems: ContentItem[] = []; - - if (revertLog != null) { - const log = await new ArchiveLog().loadFromFile(revertLog); - id = log.getData('UNARCHIVE'); - } - - if (id != null) { - const itemIds = Array.isArray(id) ? id : [id]; - const items: ContentItem[] = []; - - for (const id of itemIds) { - try { - items.push(await client.contentItems.get(id)); - } catch { - // Missing item. - } - } - - contentItems.push(...items.filter(item => item.status === Status.ACTIVE)); - - return { - contentItems, - missingContent: contentItems.length != itemIds.length - }; - } - - const hub = await client.hubs.get(hubId); - - contentItems = await getContent(client, hub, facet, { repoId, folderId, status: Status.ACTIVE, enrichItems: true }); - - return { contentItems, missingContent: false }; - } catch (err) { - console.log(err); - - return { - contentItems: [], - missingContent: false - }; - } -}; - -export const processItems = async ({ +const processItems = async ({ contentItems, - force, - silent, - logFile, - allContent, - missingContent, + log, ignoreError, ignoreSchemaValidation }: { contentItems: ContentItem[]; - force?: boolean; - silent?: boolean; - logFile: FileLog; - allContent: boolean; - missingContent: boolean; + log: FileLog; ignoreError?: boolean; ignoreSchemaValidation?: boolean; -}): Promise => { - if (contentItems.length == 0) { - console.log('Nothing found to archive, aborting.'); - return; - } - - console.log('The following content items will be archived:'); - contentItems.forEach((contentItem: ContentItem) => { - console.log(` ${contentItem.label} (${contentItem.id})`); - }); - console.log(`Total: ${contentItems.length}`); - - if (!force) { - const yes = await confirmAllContent('archive', 'content item', allContent, missingContent); - if (!yes) { - return; - } - } - - const log = logFile.open(); - +}): Promise<{ failedArchives: ContentItem[] }> => { const progress = progressBar(contentItems.length, 0, { title: 'Archiving content items' }); + const failedArchives = []; - let successCount = 0; for (let i = 0; i < contentItems.length; i++) { try { const deliveryKey = contentItems[i].body._meta.deliveryKey; @@ -206,17 +119,17 @@ export const processItems = async ({ await contentItems[i].related.archive(); progress.increment(); log.addAction('ARCHIVE', `${args}`); - successCount++; } catch (e) { + failedArchives.push(contentItems[i]); progress.increment(); log.addComment(`ARCHIVE FAILED: ${contentItems[i].id}`); log.addComment(e.toString()); if (ignoreError) { - log.warn(`Failed to archive ${contentItems[i].label} (${contentItems[i].id}), continuing.`, e); + log.warn(`\nFailed to archive ${contentItems[i].label} (${contentItems[i].id}), continuing.`, e); } else { progress.stop(); - log.error(`Failed to archive ${contentItems[i].label} (${contentItems[i].id}), aborting.`, e); + log.error(`\nFailed to archive ${contentItems[i].label} (${contentItems[i].id}), aborting.`, e); break; } } @@ -224,56 +137,78 @@ export const processItems = async ({ progress.stop(); - await log.close(!silent); - - console.log(`Archived ${successCount} content items.`); + return { failedArchives }; }; export const handler = async (argv: Arguments): Promise => { const { id, logFile, force, silent, ignoreError, hubId, revertLog, repoId, folderId, ignoreSchemaValidation } = argv; + const log = logFile.open(); const client = dynamicContentClientFactory(argv); - const facet = withOldFilters(argv.facet, argv); - const allContent = !id && !facet && !revertLog && !folderId && !repoId; if (repoId && id) { - console.log('ID of content item is specified, ignoring repository ID'); + log.appendLine('ID of content item is specified, ignoring repository ID'); } if (id && facet) { - console.log('Please specify either a facet or an ID - not both.'); + log.appendLine('Please specify either a facet or an ID - not both.'); return; } if (repoId && folderId) { - console.log('Folder is specified, ignoring repository ID'); + log.appendLine('Folder is specified, ignoring repository ID'); } if (allContent) { - console.log('No filter was given, archiving all content'); + log.appendLine('No filter was given, archiving all content'); } - const { contentItems, missingContent } = await getContentItems({ - client, - id, - hubId, - repoId, - folderId, - revertLog, - facet - }); + let ids: string[] = []; + + if (id) { + ids = Array.isArray(id) ? id : [id]; + } - await processItems({ + if (revertLog) { + const log = await new ArchiveLog().loadFromFile(revertLog); + ids = log.getData('UNARCHIVE'); + } + + const hub = await client.hubs.get(hubId); + const contentItems = ids.length + ? (await getContentByIds(client, ids)).filter(item => item.status === Status.ACTIVE) + : await getContent(client, hub, facet, { repoId, folderId, status: Status.ACTIVE, enrichItems: true }); + + if (!contentItems.length) { + log.appendLine('Nothing found to archive, aborting'); + return; + } + + const missingContentItems = ids.length > 0 ? Boolean(ids.length !== contentItems.length) : false; + log.appendLine(`Found ${contentItems.length} content items to archive`); + + if (!force) { + const yes = await confirmAllContent('archive', 'content item', allContent, missingContentItems); + if (!yes) { + return; + } + } + + const { failedArchives } = await processItems({ contentItems, - force, - silent, - logFile, - allContent, - missingContent, + log, ignoreError, ignoreSchemaValidation }); + + const failedArchiveMsg = failedArchives.length + ? `with ${failedArchives.length} failed archives - check logs for details` + : ``; + + log.appendLine(`Archived content items ${failedArchiveMsg}`); + + await log.close(!silent); }; // log format: diff --git a/src/commands/content-item/publish.spec.ts b/src/commands/content-item/publish.spec.ts index d24ec78f..a310d18a 100644 --- a/src/commands/content-item/publish.spec.ts +++ b/src/commands/content-item/publish.spec.ts @@ -1,31 +1,40 @@ -import { builder, handler, getContentItems, processItems, LOG_FILENAME, coerceLog } from './publish'; -import { Status, ContentItem, DynamicContent, Hub, PublishingJob } from 'dc-management-sdk-js'; +import { builder, handler, LOG_FILENAME, coerceLog } from './publish'; +import { ContentItem, Hub, PublishingJob, Job } from 'dc-management-sdk-js'; import { FileLog } from '../../common/file-log'; -import { Arguments } from 'yargs'; -import { ConfigurationParameters } from '../configure'; -import PublishOptions from '../../common/publish/publish-options'; import Yargs from 'yargs/yargs'; -import readline from 'readline'; import { PublishingJobStatus } from 'dc-management-sdk-js/build/main/lib/model/PublishingJobStatus'; +import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; +import { getContentByIds } from '../../common/content-item/get-content-items-by-ids'; +import { getContent } from '../../common/filter/fetch-content'; +import * as confirmAllContentModule from '../../common/content-item/confirm-all-content'; +import * as questionHelpers from '../../common/question-helpers'; const mockPublish = jest.fn().mockImplementation((contentItems, fn) => { - fn(contentItems); + fn(contentItems, new PublishingJob({ state: PublishingJobStatus.CREATED })); }); const mockCheck = jest.fn().mockImplementation((publishingJob, fn) => { fn(new PublishingJob({ state: PublishingJobStatus.COMPLETED })); }); +const mockPublishOnIdle = jest.fn().mockImplementation(() => Promise.resolve()); +const mockCheckOnIdle = jest.fn().mockImplementation(() => Promise.resolve()); + +const confirmAllContentSpy = jest.spyOn(confirmAllContentModule, 'confirmAllContent'); +const asyncQuestionSpy = jest.spyOn(questionHelpers, 'asyncQuestion'); jest.mock('../../services/dynamic-content-client-factory'); -jest.mock('../../common/content-item/confirm-all-content'); jest.mock('../../common/log-helpers'); jest.mock('../../common/filter/fetch-content'); -jest.mock('readline'); +jest.mock('../../common/content-item/get-content-items-by-ids', () => { + return { + getContentByIds: jest.fn() + }; +}); jest.mock('../../common/publishing/content-item-publishing-service', () => { return { ContentItemPublishingService: jest.fn().mockImplementation(() => { return { publish: mockPublish, - onIdle: jest.fn() + onIdle: mockPublishOnIdle }; }) }; @@ -35,29 +44,12 @@ jest.mock('../../common/publishing/content-item-publishing-job-service', () => { ContentItemPublishingJobService: jest.fn().mockImplementation(() => { return { check: mockCheck, - onIdle: jest.fn() + onIdle: mockCheckOnIdle }; }) }; }); -const mockClient = { - contentItems: { - get: jest.fn() - }, - hubs: { - get: jest.fn() - } -} as unknown as DynamicContent; - -const mockLog = { - open: jest.fn().mockReturnValue({ - appendLine: jest.fn(), - addComment: jest.fn(), - close: jest.fn() - }) -} as unknown as FileLog; - describe('publish tests', () => { describe('builder tests', () => { it('should configure yargs', function () { @@ -112,113 +104,94 @@ describe('publish tests', () => { }); }); - describe('getContentItems tests', () => { - beforeEach(() => jest.clearAllMocks()); - - it('should return content items by id', async () => { - const mockItem = { id: '1', status: Status.ACTIVE } as ContentItem; - mockClient.contentItems.get = jest.fn().mockResolvedValue(mockItem); - - const result = await getContentItems({ - client: mockClient, - id: '1', - hubId: 'hub-id' - }); - - expect(result.contentItems).toEqual([mockItem]); - expect(result.missingContent).toBe(false); - }); - - it('should filter out non-active content items', async () => { - mockClient.contentItems.get = jest - .fn() - .mockResolvedValueOnce({ id: '1', status: Status.ARCHIVED }) - .mockResolvedValueOnce({ id: '2', status: Status.ACTIVE }); - - const result = await getContentItems({ - client: mockClient, - id: ['1', '2'], - hubId: 'hub-id' - }); - - expect(result.contentItems).toHaveLength(1); - expect(result.contentItems[0].id).toBe('2'); - expect(result.missingContent).toBe(true); - }); - - it('should return content using fallback filters', async () => { - const mockHub = {} as Hub; - const contentItems = [{ id: 'a', status: Status.ACTIVE }] as ContentItem[]; - const getContent = require('../../common/filter/fetch-content').getContent; - mockClient.hubs.get = jest.fn().mockResolvedValue(mockHub); - getContent.mockResolvedValue(contentItems); - - const result = await getContentItems({ - client: mockClient, - hubId: 'hub-id', - facet: 'label:test' - }); - - expect(result.contentItems).toEqual(contentItems); - }); - }); + describe('handler', () => { + const HUB_ID = '67d1c1c7642fa239dbe15164'; + const CONTENT_ITEM_ID = 'c5b659df-680e-4711-bfbe-84eaa10d76cc'; + const globalArgs = { + $0: 'test', + _: ['test'], + json: true, + clientId: 'client-id', + clientSecret: 'client-secret', + hubId: HUB_ID + }; + + const mockAppendLine = jest.fn(); + const mockLog = { + open: jest.fn().mockReturnValue({ + appendLine: mockAppendLine, + addComment: jest.fn(), + close: jest.fn() + }) + } as unknown as FileLog; - describe('processItems tests', () => { beforeEach(() => { jest.clearAllMocks(); - jest.mock('readline'); + confirmAllContentSpy.mockResolvedValue(true); + asyncQuestionSpy.mockResolvedValue(true); }); - it('should exit early if no content items', async () => { - console.log = jest.fn(); - - await processItems({ - contentItems: [], - logFile: mockLog, - allContent: false, - missingContent: false, - client: mockClient + it('should publish content item by id', async () => { + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: CONTENT_ITEM_ID, body: { _meta: {} } }) + ]); - expect(console.log).toHaveBeenCalledWith('Nothing found to publish, aborting.'); - }); + mockPublish.mockImplementation((contentItem, fn) => { + fn(new Job({ id: '68e5289f0aba3024bde050f9', status: 'COMPLETE' })); + }); - it('should confirm before publishing when force is false', async () => { - const confirmAllContent = require('../../common/content-item/confirm-all-content').confirmAllContent; - confirmAllContent.mockResolvedValue(false); - console.log = jest.fn(); - - await processItems({ - contentItems: [new ContentItem({ id: '1', label: 'Test', body: { _meta: {} } })], - force: false, - silent: true, - logFile: mockLog, - allContent: false, - missingContent: false, - client: mockClient + await handler({ + ...globalArgs, + id: CONTENT_ITEM_ID, + logFile: mockLog }); - expect(confirmAllContent).toHaveBeenCalled(); + expect(getContentByIds).toHaveBeenCalledWith(expect.any(Object), [CONTENT_ITEM_ID]); + expect(mockPublish).toHaveBeenCalledTimes(1); + expect(mockPublish).toHaveBeenCalledWith(expect.any(ContentItem), expect.any(Function)); + expect(mockPublishOnIdle).toHaveBeenCalledTimes(1); + expect(mockCheck).toHaveBeenCalledTimes(1); + expect(mockCheckOnIdle).toHaveBeenCalledTimes(1); }); + it('should publish content items by query', async () => { + const REPOSITORY_ID = '67d1c1cf642fa239dbe15165'; + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContent as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: CONTENT_ITEM_ID, body: { _meta: {} } }) + ]); - it('should process all items and call publish', async () => { - const contentItem = new ContentItem({ id: '1', label: 'Publish Me', body: { _meta: {} } }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (readline as any).setResponses(['Y']); + mockPublish.mockImplementation((contentItem, fn) => { + fn(new Job({ id: '68e5289f0aba3024bde050f9', status: 'COMPLETE' })); + }); - await processItems({ - contentItems: [contentItem], - force: true, - silent: true, - logFile: mockLog, - allContent: false, - missingContent: false, - client: mockClient + await handler({ + ...globalArgs, + repoId: REPOSITORY_ID, + logFile: mockLog }); + expect(getContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Hub), undefined, { + enrichItems: true, + folderId: undefined, + repoId: REPOSITORY_ID, + status: 'ACTIVE' + }); expect(mockPublish).toHaveBeenCalledTimes(1); + expect(mockPublish).toHaveBeenCalledWith(expect.any(ContentItem), expect.any(Function)); + expect(mockPublishOnIdle).toHaveBeenCalledTimes(1); expect(mockCheck).toHaveBeenCalledTimes(1); + expect(mockCheckOnIdle).toHaveBeenCalledTimes(1); }); it('should process all items while filtering out any dependencies and call publish', async () => { @@ -239,55 +212,114 @@ describe('publish tests', () => { label: 'No need to publish me', body: { _meta: {} } }); + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([contentItemWithDependency, contentItemDependency]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (readline as any).setResponses(['Y']); - - await processItems({ - contentItems: [contentItemWithDependency, contentItemDependency], - force: true, - silent: true, - logFile: mockLog, - allContent: false, - missingContent: false, - client: mockClient + mockPublish.mockImplementation((contentItem, fn) => { + fn(new Job({ id: '68e5289f0aba3024bde050f9', status: 'COMPLETE' })); + }); + + await handler({ + ...globalArgs, + id: [contentItemWithDependency.id, contentItemDependency.id], + logFile: mockLog }); expect(mockPublish).toHaveBeenCalledTimes(1); + expect(mockPublish).toHaveBeenCalledWith(contentItemWithDependency, expect.any(Function)); + expect(mockPublishOnIdle).toHaveBeenCalledTimes(1); expect(mockCheck).toHaveBeenCalledTimes(1); + expect(mockCheckOnIdle).toHaveBeenCalledTimes(1); }); - }); - describe('handler tests', () => { - const clientFactory = require('../../services/dynamic-content-client-factory').default; - const getItemsSpy = jest.spyOn(require('./publish'), 'getContentItems'); - const processSpy = jest.spyOn(require('./publish'), 'processItems'); - beforeEach(() => { - jest.clearAllMocks(); - clientFactory.mockReturnValue(mockClient); - getItemsSpy.mockResolvedValue({ - contentItems: [{ id: '123', label: 'Test', status: Status.ACTIVE }], - missingContent: false + it('should exit before processing content items if confirmation to proceed is rejected', async () => { + confirmAllContentSpy.mockResolvedValue(false); + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: CONTENT_ITEM_ID, body: { _meta: {} } }) + ]); + + await handler({ + ...globalArgs, + id: CONTENT_ITEM_ID, + logFile: mockLog + }); + expect(mockPublish).not.toHaveBeenCalled(); + expect(mockPublishOnIdle).not.toHaveBeenCalled(); + expect(mockCheck).not.toHaveBeenCalled(); + expect(mockCheckOnIdle).not.toHaveBeenCalled(); + }); + + it('should not check publishing jobs if check question is rejected', async () => { + asyncQuestionSpy.mockResolvedValue(false); + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } }); - processSpy.mockResolvedValue(undefined); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: 'CONTENT_ITEM_ID_ZZZZZZZZZ', body: { _meta: {} } }) + ]); + + mockPublish.mockImplementation((contentItem, fn) => { + fn(new Job({ id: '68e5289f0aba3024bde050f9', status: 'COMPLETE' })); + }); + + await handler({ + ...globalArgs, + id: 'CONTENT_ITEM_ID_ZZZZZZZZZ', + logFile: mockLog + }); + + expect(mockPublish).toHaveBeenCalledTimes(1); + expect(mockPublishOnIdle).toHaveBeenCalledTimes(1); + expect(mockCheck).not.toHaveBeenCalled(); + expect(mockCheckOnIdle).not.toHaveBeenCalled(); }); - it('should warn when both id and facet are provided', async () => { - console.log = jest.fn(); + + it('should exit early if ID or query args are not passed', async () => { await handler({ - id: '1', - facet: 'label:test', - hubId: 'hub-id', + ...globalArgs, + id: CONTENT_ITEM_ID, + facet: 'mock-facet', logFile: mockLog - } as Arguments); - expect(console.log).toHaveBeenCalledWith('Please specify either a facet or an ID - not both.'); + }); + expect(mockAppendLine).toHaveBeenCalledWith('Please specify either a facet or an ID - not both'); + expect(mockPublish).not.toHaveBeenCalled(); + expect(mockPublishOnIdle).not.toHaveBeenCalled(); + expect(mockCheck).not.toHaveBeenCalled(); + expect(mockCheckOnIdle).not.toHaveBeenCalled(); }); - it('should process items with valid inputs', async () => { + + it('should exit early if no content items', async () => { + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([]); await handler({ - hubId: 'hub-id', + ...globalArgs, + id: CONTENT_ITEM_ID, logFile: mockLog - } as Arguments); - expect(getItemsSpy).toHaveBeenCalled(); - expect(processSpy).toHaveBeenCalled(); + }); + expect(mockAppendLine).toHaveBeenCalledWith('Nothing found to publish, aborting'); + expect(mockPublish).not.toHaveBeenCalled(); + expect(mockPublishOnIdle).not.toHaveBeenCalled(); + expect(mockCheck).not.toHaveBeenCalled(); + expect(mockCheckOnIdle).not.toHaveBeenCalled(); }); }); }); diff --git a/src/commands/content-item/publish.ts b/src/commands/content-item/publish.ts index 526b7386..ffc58820 100644 --- a/src/commands/content-item/publish.ts +++ b/src/commands/content-item/publish.ts @@ -1,21 +1,20 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { Arguments, Argv } from 'yargs'; import { ConfigurationParameters } from '../configure'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { confirmAllContent } from '../../common/content-item/confirm-all-content'; import PublishOptions from '../../common/publish/publish-options'; -import { ContentItem, ContentRepository, DynamicContent, PublishingJob, Status } from 'dc-management-sdk-js'; +import { ContentItem, DynamicContent, PublishingJob, Status } from 'dc-management-sdk-js'; import { getDefaultLogPath, createLog } from '../../common/log-helpers'; import { FileLog } from '../../common/file-log'; import { withOldFilters } from '../../common/filter/facet'; import { getContent } from '../../common/filter/fetch-content'; import { asyncQuestion } from '../../common/question-helpers'; -import { ContentDependancyTree } from '../../common/content-item/content-dependancy-tree'; -import { ContentMapping } from '../../common/content-mapping'; import { ContentItemPublishingService } from '../../common/publishing/content-item-publishing-service'; import { ContentItemPublishingJobService } from '../../common/publishing/content-item-publishing-job-service'; import { PublishingJobStatus } from 'dc-management-sdk-js/build/main/lib/model/PublishingJobStatus'; import { progressBar } from '../../common/progress-bar/progress-bar'; +import { getContentByIds } from '../../common/content-item/get-content-items-by-ids'; +import { dedupeContentItems } from '../../common/content-item/dedupe-content-items'; export const command = 'publish [id]'; @@ -72,122 +71,27 @@ export const builder = (yargs: Argv): void => { }); }; -export const getContentItems = async ({ - client, - id, - hubId, - repoId, - folderId, - facet -}: { - client: DynamicContent; - id?: string | string[]; - hubId: string; - repoId?: string | string[]; - folderId?: string | string[]; - facet?: string; -}): Promise<{ contentItems: ContentItem[]; missingContent: boolean }> => { - try { - let contentItems: ContentItem[] = []; - - if (id != null) { - const itemIds = Array.isArray(id) ? id : [id]; - const items: ContentItem[] = []; - - for (const id of itemIds) { - try { - items.push(await client.contentItems.get(id)); - } catch { - // Missing item. - } - } - - contentItems.push(...items.filter(item => item.status === Status.ACTIVE)); - - return { - contentItems, - missingContent: contentItems.length != itemIds.length - }; - } - - const hub = await client.hubs.get(hubId); - - contentItems = await getContent(client, hub, facet, { repoId, folderId, status: Status.ACTIVE, enrichItems: true }); - - return { contentItems, missingContent: false }; - } catch (err) { - console.log(err); - - return { - contentItems: [], - missingContent: false - }; - } -}; - -export const processItems = async ({ +const processItems = async ({ client, contentItems, force, - silent, - logFile, - allContent, - missingContent + log }: { client: DynamicContent; contentItems: ContentItem[]; force?: boolean; - silent?: boolean; - logFile: FileLog; - allContent: boolean; - missingContent: boolean; + log: FileLog; }): Promise => { - if (contentItems.length == 0) { - console.log('Nothing found to publish, aborting.'); - return; - } - - const repoContentItems = contentItems.map(content => ({ repo: new ContentRepository(), content })); - const contentTree = new ContentDependancyTree(repoContentItems, new ContentMapping()); - let childCount = 0; - const rootContentItems = contentTree.all - .filter(node => { - let isTopLevel = true; - - contentTree.traverseDependants( - node, - dependant => { - if (dependant != node && contentTree.all.findIndex(entry => entry === dependant) !== -1) { - isTopLevel = false; - childCount++; - } - }, - true - ); - - return isTopLevel; - }) - .map(node => node.owner.content); + const dedupedContentItems = dedupeContentItems(contentItems); - const log = logFile.open(); log.appendLine( - `Found ${rootContentItems.length} item(s) to publish (ignoring ${childCount} duplicate child item(s)).` + `Publishing ${dedupedContentItems.length} item(s) (ignoring ${contentItems.length - dedupedContentItems.length} duplicate child item(s))` ); - if (!force) { - const yes = await confirmAllContent('publish', 'content items', allContent, missingContent); - if (!yes) { - return; - } - } - - log.appendLine(`Publishing ${rootContentItems.length} item(s).`); - const publishingService = new ContentItemPublishingService(); const contentItemPublishJobs: [ContentItem, PublishingJob][] = []; - const publishProgress = progressBar(rootContentItems.length, 0, { title: 'Publishing content items' }); - - for (const item of rootContentItems) { + const publishProgress = progressBar(dedupedContentItems.length, 0, { title: 'Publishing content items' }); + for (const item of dedupedContentItems) { try { await publishingService.publish(item, (contentItem, publishingJob) => { contentItemPublishJobs.push([contentItem, publishingJob]); @@ -229,53 +133,68 @@ export const processItems = async ({ await publishingJobService.onIdle(); checkPublishProgress.stop(); } - - log.appendLine(`Publishing complete`); - - await log.close(!silent); }; export const handler = async (argv: Arguments): Promise => { const { id, logFile, force, silent, hubId, repoId, folderId } = argv; + const log = logFile.open(); const client = dynamicContentClientFactory(argv); - const facet = withOldFilters(argv.facet, argv); - const allContent = !id && !facet && !folderId && !repoId; if (repoId && id) { - console.log('ID of content item is specified, ignoring repository ID'); + log.appendLine('ID of content item is specified, ignoring repository ID'); } if (id && facet) { - console.log('Please specify either a facet or an ID - not both.'); + log.appendLine('Please specify either a facet or an ID - not both'); return; } if (repoId && folderId) { - console.log('Folder is specified, ignoring repository ID'); + log.appendLine('Folder is specified, ignoring repository ID'); } if (allContent) { - console.log('No filter was given, publishing all content'); + log.appendLine('No filter was given, publishing all content'); } - const { contentItems, missingContent } = await getContentItems({ - client, - id, - hubId, - repoId, - folderId, - facet - }); + let ids: string[] = []; + + if (id) { + ids = Array.isArray(id) ? id : [id]; + } + + const hub = await client.hubs.get(hubId); + const contentItems = + ids.length > 0 + ? await getContentByIds(client, ids) + : await getContent(client, hub, facet, { repoId, folderId, status: Status.ACTIVE, enrichItems: true }); + + if (!contentItems.length) { + log.appendLine('Nothing found to publish, aborting'); + return; + } + + const missingContentItems = ids.length > 0 ? Boolean(ids.length !== contentItems.length) : false; + + log.appendLine(`Found ${contentItems.length} content items to publish (including duplicate child items)\n`); + + if (!force) { + const yes = await confirmAllContent('publish', 'content items', allContent, missingContentItems); + if (!yes) { + return; + } + } await processItems({ client, contentItems, force, - silent, - logFile, - allContent, - missingContent + log }); + + log.appendLine(`Publishing complete`); + + await log.close(!silent); }; diff --git a/src/commands/content-item/sync.ts b/src/commands/content-item/sync.ts index 94d66b74..05827ae8 100644 --- a/src/commands/content-item/sync.ts +++ b/src/commands/content-item/sync.ts @@ -83,8 +83,8 @@ export const handler = async (argv: Arguments ({ + ...jest.requireActual('../../common/log-helpers'), + getDefaultLogPath: jest.fn() +})); + jest.mock('../../common/filter/fetch-content'); describe('content-item unarchive command', () => { @@ -27,7 +36,8 @@ describe('content-item unarchive command', () => { const config = { clientId: 'client-id', clientSecret: 'client-id', - hubId: 'hub-id' + hubId: 'hub-id', + logFile: new FileLog() }; const mockValues = ( @@ -41,7 +51,7 @@ describe('content-item unarchive command', () => { mockItemGetById: () => void; mockRepoGet: () => void; mockFolderGet: () => void; - mockFacet: () => void; + mockGetContent: () => void; contentItems: ContentItem[]; } => { const mockGet = jest.fn(); @@ -52,7 +62,7 @@ describe('content-item unarchive command', () => { const mockItemGetById = jest.fn(); const mockRepoGet = jest.fn(); const mockFolderGet = jest.fn(); - const mockFacet = jest.spyOn(fetchContentModule, 'getContent') as jest.Mock; + const mockGetContent = jest.spyOn(fetchContentModule, 'getContent') as jest.Mock; const item = new ContentItem({ id: '1', @@ -196,7 +206,7 @@ describe('content-item unarchive command', () => { mockItemUpdate.mockResolvedValue(item); mockItemsList.mockResolvedValue(new MockPage(ContentItem, contentItems)); - mockFacet.mockResolvedValue(contentItems); + mockGetContent.mockResolvedValue(contentItems); if (unarchiveError) { mockUnarchive.mockRejectedValue(new Error('Error')); @@ -213,7 +223,7 @@ describe('content-item unarchive command', () => { mockItemGetById, mockRepoGet, mockFolderGet, - mockFacet, + mockGetContent, contentItems }; }; @@ -282,7 +292,8 @@ describe('content-item unarchive command', () => { expect(spyOption).toHaveBeenCalledWith('logFile', { type: 'string', default: LOG_FILENAME, - describe: 'Path to a log file to write to.' + describe: 'Path to a log file to write to.', + coerce: coerceLog }); expect(spyOption).toHaveBeenCalledWith('name', { @@ -308,11 +319,24 @@ describe('content-item unarchive command', () => { jest.clearAllMocks(); }); + it('should use getDefaultLogPath for LOG_FILENAME with process.platform as default', function () { + LOG_FILENAME(); + + expect(getDefaultLogPath).toHaveBeenCalledWith('content-item', 'unarchive', process.platform); + }); + + it('should generate a log with coerceLog with the appropriate title', function () { + const logFile = coerceLog('filename.log'); + + expect(logFile).toEqual(expect.any(FileLog)); + expect(logFile.title).toMatch(/^dc\-cli test\-ver \- Content Items Unarchive Log \- ./); + }); + it('should unarchive all content', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockGet, mockFacet, mockUnarchive } = mockValues(); + const { mockGet, mockGetContent, mockUnarchive } = mockValues(); const argv = { ...yargArgs, @@ -321,7 +345,7 @@ describe('content-item unarchive command', () => { await handler(argv); expect(mockGet).toHaveBeenCalled(); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ARCHIVED }); expect(mockUnarchive).toHaveBeenCalledTimes(2); @@ -366,7 +390,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -375,7 +399,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { repoId: 'repo1', status: Status.ARCHIVED }); @@ -386,7 +410,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -395,7 +419,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { repoId: ['repo1', 'repo2'], status: Status.ARCHIVED }); @@ -406,7 +430,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -416,7 +440,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { repoId: 'repo123', folderId: 'folder1', status: Status.ARCHIVED @@ -428,7 +452,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -437,7 +461,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { folderId: ['folder1', 'folder1'], status: Status.ARCHIVED }); @@ -448,7 +472,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -458,7 +482,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { folderId: 'folder1', status: Status.ARCHIVED }); @@ -469,7 +493,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFolderGet, mockItemsList, mockFacet } = mockValues(); + const { mockUnarchive, mockFolderGet, mockItemsList, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -479,7 +503,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).not.toHaveBeenCalled(); + expect(mockGetContent).not.toHaveBeenCalled(); expect(mockFolderGet).not.toHaveBeenCalled(); expect(mockItemsList).not.toHaveBeenCalled(); expect(mockUnarchive).not.toHaveBeenCalled(); @@ -489,10 +513,10 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); - (mockFacet as jest.Mock).mockReset(); - (mockFacet as jest.Mock).mockResolvedValue([]); + (mockGetContent as jest.Mock).mockReset(); + (mockGetContent as jest.Mock).mockResolvedValue([]); const argv = { ...yargArgs, @@ -502,7 +526,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item3', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item3', { folderId: 'folder1', status: Status.ARCHIVED }); @@ -513,7 +537,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['n']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -523,7 +547,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'name:item1', { folderId: 'folder1', status: Status.ARCHIVED }); @@ -534,7 +558,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -543,7 +567,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'schema:http://test.com', { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'schema:http://test.com', { status: Status.ARCHIVED }); expect(mockUnarchive).toHaveBeenCalledTimes(2); @@ -553,7 +577,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(true); + const { mockUnarchive, mockGetContent } = mockValues(true); const argv = { ...yargArgs, @@ -562,7 +586,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ARCHIVED }); expect(mockUnarchive).toHaveBeenCalledTimes(2); @@ -572,7 +596,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const { mockUnarchive, mockFacet } = mockValues(true); + const { mockUnarchive, mockGetContent } = mockValues(true); const argv = { ...yargArgs, @@ -581,7 +605,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ARCHIVED }); expect(mockUnarchive).toHaveBeenCalledTimes(1); @@ -591,7 +615,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['input', 'ignored']); - const { mockUnarchive, mockFacet } = mockValues(); + const { mockUnarchive, mockGetContent } = mockValues(); const argv = { ...yargArgs, @@ -600,7 +624,7 @@ describe('content-item unarchive command', () => { }; await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { status: Status.ARCHIVED }); expect(mockUnarchive).toHaveBeenCalledTimes(2); @@ -614,7 +638,7 @@ describe('content-item unarchive command', () => { const log = '// Type log test file\n' + 'ARCHIVE 1\n' + 'ARCHIVE 2 delivery-key\n' + 'ARCHIVE idMissing\n'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); @@ -629,7 +653,6 @@ describe('content-item unarchive command', () => { const argv = { ...yargArgs, ...config, - logFile: LOG_FILENAME(), silent: true, force: true, revertLog: logFileName @@ -657,7 +680,7 @@ describe('content-item unarchive command', () => { 'ARCHIVE idMissing\n'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); @@ -672,7 +695,6 @@ describe('content-item unarchive command', () => { const argv = { ...yargArgs, ...config, - logFile: LOG_FILENAME(), silent: true, force: true, revertLog: logFileName @@ -702,7 +724,7 @@ describe('content-item unarchive command', () => { 'ARCHIVE idMissing\n'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); @@ -717,7 +739,6 @@ describe('content-item unarchive command', () => { const argv = { ...yargArgs, ...config, - logFile: LOG_FILENAME(), silent: true, force: true, revertLog: logFileName @@ -740,19 +761,20 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['input', 'ignored']); - const { mockFacet, mockUnarchive } = mockValues(true); + const { mockGetContent, mockUnarchive } = mockValues(true); - (mockFacet as jest.Mock).mockReset(); - (mockFacet as jest.Mock).mockRejectedValue(new Error('Simulated Error')); + (mockGetContent as jest.Mock).mockReset(); + (mockGetContent as jest.Mock).mockRejectedValue(new Error('Simulated error')); const argv = { ...yargArgs, ...config, folderId: 'folder1' }; - await handler(argv); - expect(mockFacet).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { + await expect(handler(argv)).rejects.toThrowErrorMatchingInlineSnapshot(`"Simulated error"`); + + expect(mockGetContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), undefined, { folderId: 'folder1', status: Status.ARCHIVED }); @@ -763,7 +785,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - if (await promisify(exists)(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`)) { + if (existsSync(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`)) { await promisify(unlink)(`temp_${process.env.JEST_WORKER_ID}/content-item-archive.log`); } @@ -771,25 +793,26 @@ describe('content-item unarchive command', () => { const log = '// Type log test file\n' + 'ARCHIVE 1\n' + 'ARCHIVE 2\n' + 'ARCHIVE idMissing'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); - const { mockUnarchive, mockItemGetById, mockFacet } = mockValues(true); + const { mockUnarchive, mockItemGetById, mockGetContent } = mockValues(true); const argv = { ...yargArgs, ...config, - logFile: LOG_FILENAME(), silent: true, force: true, revertLog: 'wrongFileName.log' }; - await handler(argv); + await expect(handler(argv)).rejects.toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open 'wrongFileName.log'"` + ); expect(mockItemGetById).not.toHaveBeenCalled(); - expect(mockFacet).not.toHaveBeenCalled(); + expect(mockGetContent).not.toHaveBeenCalled(); expect(mockUnarchive).not.toHaveBeenCalled(); }); @@ -797,7 +820,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - if (await promisify(exists)(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`)) { + if (existsSync(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`)) { await promisify(unlink)(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`); } @@ -806,7 +829,7 @@ describe('content-item unarchive command', () => { const argv = { ...yargArgs, ...config, - logFile: `temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`, + logFile: createLog(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`), id: '1' }; @@ -815,7 +838,7 @@ describe('content-item unarchive command', () => { expect(mockItemGetById).toHaveBeenCalled(); expect(mockUnarchive).toHaveBeenCalled(); - const logExists = await promisify(exists)(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`); + const logExists = existsSync(`temp_${process.env.JEST_WORKER_ID}/content-item-unarchive.log`); expect(logExists).toBeTruthy(); @@ -842,7 +865,7 @@ describe('content-item unarchive command', () => { const log = '// Type log test file\n' + 'ARCHIVE 1 delivery-key\n'; const dir = dirname(logFileName); - if (!(await promisify(exists)(dir))) { + if (!existsSync(dir)) { await promisify(mkdir)(dir); } await promisify(writeFile)(logFileName, log); @@ -879,7 +902,6 @@ describe('content-item unarchive command', () => { const argv = { ...yargArgs, ...config, - logFile: LOG_FILENAME(), silent: true, force: true, revertLog: logFileName, @@ -891,109 +913,30 @@ describe('content-item unarchive command', () => { expect(mockItemUpdate).toHaveBeenCalledTimes(1); expect((mockItemUpdate as jest.Mock).mock.calls[0][1].ignoreSchemaValidation).toBe(true); }); - }); - - describe('getContentItems tests', () => { - beforeEach(() => { - const { mockItemGetById, contentItems } = mockValues(); - - (mockItemGetById as jest.Mock).mockReset(); - (mockItemGetById as jest.Mock).mockResolvedValueOnce(contentItems[0]); - }); - it('should get content items by id', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - id: '1', - hubId: 'hub1' - }); - - if (result) { - expect(result.contentItems.length).toBeGreaterThanOrEqual(1); - - expect(result.contentItems[0].id).toMatch('1'); - } - }); - - it('should get content items all', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - hubId: 'hub1' - }); - - if (result) { - expect(result.contentItems.length).toBe(2); - } - }); - - it('should get content items by repo', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - hubId: 'hub1', - repoId: 'repo1' - }); - - if (result) { - expect(result.contentItems.length).toBe(2); - } - }); - - it('should get content items by folder', async () => { - const result = await getContentItems({ - client: dynamicContentClientFactory({ - ...config, - ...yargArgs - }), - hubId: 'hub1', - folderId: 'folder1' - }); - if (result) { - expect(result.contentItems.length).toBe(2); - } - }); - }); - - describe('processItems tests', () => { - it('should unarchive content items', async () => { - const { contentItems, mockUnarchive } = mockValues(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (readline as any).setResponses(['y']); - - await processItems({ - contentItems, - allContent: true, - missingContent: false, - logFile: './logFile.log' + it('should not archive content items', async () => { + const { mockItemGetById, mockUnarchive } = mockValues(); + const logFile = new FileLog(); + const mockAppendFile = jest.fn(); + logFile.open = jest.fn().mockImplementation(() => { + return { + appendLine: mockAppendFile + }; }); + const argv = { + ...yargArgs, + ...config, + id: 'repo123', + logFile + }; - expect(mockUnarchive).toHaveBeenCalledTimes(2); - - if (await promisify(exists)('./logFile.log')) { - await promisify(unlink)('./logFile.log'); - } - }); - - it('should not unarchive content items', async () => { - jest.spyOn(global.console, 'log'); + (mockItemGetById as jest.Mock).mockResolvedValue([]); - await processItems({ - contentItems: [], - allContent: true, - missingContent: false - }); + await handler(argv); - expect(console.log).toHaveBeenCalled(); - expect(console.log).toHaveBeenLastCalledWith('Nothing found to unarchive, aborting.'); + expect(mockUnarchive).not.toHaveBeenCalled(); + expect(mockAppendFile).toHaveBeenCalled(); + expect(mockAppendFile).toHaveBeenLastCalledWith('Nothing found to unarchive, aborting'); }); }); }); diff --git a/src/commands/content-item/unarchive.ts b/src/commands/content-item/unarchive.ts index 2b58d8db..4f032dd2 100644 --- a/src/commands/content-item/unarchive.ts +++ b/src/commands/content-item/unarchive.ts @@ -3,12 +3,15 @@ import { ConfigurationParameters } from '../configure'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { ArchiveLog } from '../../common/archive/archive-log'; import { confirmAllContent } from '../../common/content-item/confirm-all-content'; -import UnarchiveOptions from '../../common/archive/unarchive-options'; import { ContentItem, DynamicContent, Status } from 'dc-management-sdk-js'; -import { getDefaultLogPath } from '../../common/log-helpers'; -import { withOldFilters } from '../../common/filter/facet'; +import { createLog, getDefaultLogPath } from '../../common/log-helpers'; +import { Facet, withOldFilters } from '../../common/filter/facet'; import { getContent } from '../../common/filter/fetch-content'; import { isEqual } from 'lodash'; +import { getContentByIds } from '../../common/content-item/get-content-items-by-ids'; +import { FileLog } from '../../common/file-log'; +import { progressBar } from '../../common/progress-bar/progress-bar'; +import ContentItemUnarchiveOptions from '../../common/archive/content-item-unarchive-options'; export const command = 'unarchive [id]'; @@ -17,6 +20,8 @@ export const desc = 'Unarchive Content Items'; export const LOG_FILENAME = (platform: string = process.platform): string => getDefaultLogPath('content-item', 'unarchive', platform); +export const coerceLog = (logFile: string): FileLog => createLog(logFile, 'Content Items Unarchive Log'); + export const builder = (yargs: Argv): void => { yargs .positional('id', { @@ -65,7 +70,8 @@ export const builder = (yargs: Argv): void => { .option('logFile', { type: 'string', default: LOG_FILENAME, - describe: 'Path to a log file to write to.' + describe: 'Path to a log file to write to.', + coerce: coerceLog }) .option('name', { type: 'string', @@ -82,126 +88,78 @@ export const builder = (yargs: Argv): void => { }); }; -export const getContentItems = async ({ +const getContentToUnarchiveWithIds = async ({ client, - id, - hubId, - repoId, - folderId, - revertLog, - facet + ids, + revertItems }: { client: DynamicContent; - id?: string | string[]; - hubId: string; - repoId?: string | string[]; - folderId?: string | string[]; - revertLog?: string; - facet?: string; -}): Promise<{ contentItems: ContentItem[]; missingContent: boolean }> => { - try { - let contentItems: ContentItem[] = []; - let revertItems: string[][] = []; - - if (revertLog != null) { - const log = await new ArchiveLog().loadFromFile(revertLog); - revertItems = log.getData('ARCHIVE').map(args => args.split(' ')); - id = revertItems.map(item => item[0]); - } + ids: string[]; + revertItems?: string[][]; +}) => { + let contentItemIds = ids; - if (id) { - const itemIds = Array.isArray(id) ? id : [id]; - const items: ContentItem[] = []; - - for (let i = 0; i < itemIds.length; i++) { - try { - const contentItem = await client.contentItems.get(itemIds[i]); - - if (revertItems.length === itemIds.length) { - contentItem.body._meta.deliveryKey = revertItems[i][1] || null; - const archivedDeliveryKeys: string[] = revertItems[i][2] ? revertItems[i][2]?.split(',') : []; - if (archivedDeliveryKeys?.length) { - contentItem.body._meta.deliveryKeys = { - values: archivedDeliveryKeys.map(deliveryKey => ({ value: deliveryKey })) - }; - } - } - items.push(contentItem); - } catch { - // Missing item. - } - } - - contentItems.push(...items.filter(item => item.status === Status.ARCHIVED)); + if (revertItems?.length) { + contentItemIds = revertItems.map(item => item[0]); + } - return { - contentItems, - missingContent: contentItems.length != itemIds.length - }; + const contentItems = await getContentByIds(client, contentItemIds); + const contentItemsWithRevert = contentItems.map(item => { + const revertItem = revertItems?.find(revertItem => item.id === revertItem[0]); + if (revertItem) { + const [, key, keys] = revertItem; + const deliveryKeys = keys?.split(',') || []; + item.body._meta.deliveryKey = key || null; + if (keys?.length) { + item.body._meta.deliveryKeys = { + values: deliveryKeys.map(k => ({ value: k })) + }; + } } + return item; + }); - const hub = await client.hubs.get(hubId); - - contentItems = await getContent(client, hub, facet, { repoId, folderId, status: Status.ARCHIVED }); - - // Delete the delivery keys, as the unarchive will attempt to reassign them if present. - contentItems.forEach(item => { - delete item.body._meta.deliveryKey; - delete item.body._meta.deliveryKeys; - }); + return contentItemsWithRevert.filter(item => item.status === Status.ARCHIVED); +}; - return { contentItems, missingContent: false }; - } catch (err) { - console.log(err); +const getContentToUnarchiveWithFacet = async ({ + client, + hubId, + facet, + repoId, + folderId +}: { + client: DynamicContent; + hubId: string; + facet?: Facet | string | undefined; + repoId?: string | string[]; + folderId?: string | string[]; +}) => { + const hub = await client.hubs.get(hubId); + const contentItems = await getContent(client, hub, facet, { repoId, folderId, status: Status.ARCHIVED }); + + // Delete the delivery keys, as the unarchive will attempt to reassign them if present. + contentItems.forEach(item => { + delete item.body._meta.deliveryKey; + delete item.body._meta.deliveryKeys; + }); - return { - contentItems: [], - missingContent: false - }; - } + return contentItems; }; -export const processItems = async ({ +const processItems = async ({ contentItems, - force, - silent, - logFile, - allContent, - missingContent, + log, ignoreError, ignoreSchemaValidation }: { contentItems: ContentItem[]; - force?: boolean; - silent?: boolean; - logFile?: string; - allContent: boolean; - missingContent: boolean; + log: FileLog; ignoreError?: boolean; ignoreSchemaValidation?: boolean; -}): Promise => { - if (contentItems.length == 0) { - console.log('Nothing found to unarchive, aborting.'); - return; - } - - console.log('The following content items will be unarchived:'); - contentItems.forEach((contentItem: ContentItem) => { - console.log(` ${contentItem.label} (${contentItem.id})`); - }); - console.log(`Total: ${contentItems.length}`); - - if (!force) { - const yes = await confirmAllContent('unarchive', 'content item', allContent, missingContent); - if (!yes) { - return; - } - } - - const timestamp = Date.now().toString(); - const log = new ArchiveLog(`Content Items Unarchive Log - ${timestamp}\n`); - - let successCount = 0; +}): Promise<{ failedUnarchives: ContentItem[] }> => { + const progress = progressBar(contentItems.length, 0, { title: 'Unarchiving content items' }); + const failedUnarchives: ContentItem[] = []; for (let i = 0; i < contentItems.length; i++) { try { @@ -221,71 +179,100 @@ export const processItems = async ({ } log.addAction('UNARCHIVE', `${contentItems[i].id}\n`); - successCount++; + progress.increment(); } catch (e) { + failedUnarchives.push(contentItems[i]); + progress.increment(); log.addComment(`UNARCHIVE FAILED: ${contentItems[i].id}`); log.addComment(e.toString()); if (ignoreError) { log.warn(`Failed to unarchive ${contentItems[i].label} (${contentItems[i].id}), continuing.`, e); } else { + progress.stop(); log.error(`Failed to unarchive ${contentItems[i].label} (${contentItems[i].id}), aborting.`, e); break; } } } - if (!silent && logFile) { - await log.writeToFile(logFile.replace('', timestamp)); - } + progress.stop(); - console.log(`Unarchived ${successCount} content items.`); + return { failedUnarchives }; }; -export const handler = async (argv: Arguments): Promise => { +export const handler = async ( + argv: Arguments +): Promise => { const { id, logFile, force, silent, ignoreError, hubId, revertLog, repoId, folderId, ignoreSchemaValidation } = argv; + const log = logFile.open(); const facet = withOldFilters(argv.facet, argv); const client = dynamicContentClientFactory(argv); - const allContent = !id && !facet && !revertLog && !folderId && !repoId; if (repoId && id) { - console.log('ID of content item is specified, ignoring repository ID'); + log.appendLine('ID of content item is specified, ignoring repository ID'); } if (id && facet) { - console.log('Please specify either a facet or an ID - not both.'); + log.appendLine('Please specify either a facet or an ID - not both.'); return; } if (repoId && folderId) { - console.log('Folder is specified, ignoring repository ID'); + log.appendLine('Folder is specified, ignoring repository ID'); } if (allContent) { - console.log('No filter was given, archiving all content'); + log.appendLine('No filter was given, archiving all content'); } - const { contentItems, missingContent } = await getContentItems({ - client, - id, - hubId, - repoId, - folderId, - revertLog, - facet - }); + let ids: string[] = []; + let revertItems: string[][] = []; + + if (id) { + ids = Array.isArray(id) ? id : [id]; + } + + if (revertLog) { + const log = await new ArchiveLog().loadFromFile(revertLog); + revertItems = log.getData('ARCHIVE').map(args => args.split(' ')); + ids = revertItems.map(item => item[0]); + } - await processItems({ + const contentItems = ids.length + ? await getContentToUnarchiveWithIds({ client, ids, revertItems }) + : await getContentToUnarchiveWithFacet({ client, hubId, facet, repoId, folderId }); + + if (!contentItems.length) { + log.appendLine('Nothing found to unarchive, aborting'); + return; + } + + const missingContentItems = ids.length > 0 ? Boolean(ids.length !== contentItems.length) : false; + logFile.appendLine(`Found ${contentItems.length} content items to unarchive`); + + if (!force) { + const yes = await confirmAllContent('unarchive', 'content item', allContent, missingContentItems); + if (!yes) { + return; + } + } + + const { failedUnarchives } = await processItems({ contentItems, - force, - silent, - logFile, - allContent, - missingContent, + log, ignoreError, ignoreSchemaValidation }); + + const failedUnarchiveMsg = failedUnarchives.length + ? `with ${failedUnarchives.length} failed archives - check logs for details` + : ``; + + log.appendLine(`Unarchived content items ${failedUnarchiveMsg}`); + + await log.close(!silent); }; // log format: diff --git a/src/commands/content-item/unpublish.spec.ts b/src/commands/content-item/unpublish.spec.ts index b5189a7f..dfcc4fd0 100644 --- a/src/commands/content-item/unpublish.spec.ts +++ b/src/commands/content-item/unpublish.spec.ts @@ -1,48 +1,41 @@ -import { builder, handler, getContentItems, processItems, LOG_FILENAME, coerceLog } from './unpublish'; -import { Status, ContentItem, DynamicContent, Hub } from 'dc-management-sdk-js'; +import { builder, handler, LOG_FILENAME, coerceLog } from './unpublish'; +import { ContentItem, Hub, PublishingJob, Job, ContentItemPublishingStatus } from 'dc-management-sdk-js'; import { FileLog } from '../../common/file-log'; -import { Arguments } from 'yargs'; -import { ConfigurationParameters } from '../configure'; -import PublishOptions from '../../common/publish/publish-options'; import Yargs from 'yargs/yargs'; +import { PublishingJobStatus } from 'dc-management-sdk-js/build/main/lib/model/PublishingJobStatus'; +import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; +import { getContentByIds } from '../../common/content-item/get-content-items-by-ids'; +import { getContent } from '../../common/filter/fetch-content'; +import * as confirmAllContentModule from '../../common/content-item/confirm-all-content'; +import * as questionHelpers from '../../common/question-helpers'; const mockUnpublish = jest.fn().mockImplementation((contentItems, fn) => { - fn(contentItems); + fn(contentItems, new PublishingJob({ state: PublishingJobStatus.CREATED })); }); +const mockUnpublishOnIdle = jest.fn().mockImplementation(() => Promise.resolve()); + +const confirmAllContentSpy = jest.spyOn(confirmAllContentModule, 'confirmAllContent'); +const asyncQuestionSpy = jest.spyOn(questionHelpers, 'asyncQuestion'); jest.mock('../../services/dynamic-content-client-factory'); -jest.mock('../../common/content-item/confirm-all-content'); jest.mock('../../common/log-helpers'); jest.mock('../../common/filter/fetch-content'); -jest.mock('readline'); +jest.mock('../../common/content-item/get-content-items-by-ids', () => { + return { + getContentByIds: jest.fn() + }; +}); jest.mock('../../common/publishing/content-item-unpublishing-service', () => { return { ContentItemUnpublishingService: jest.fn().mockImplementation(() => { return { unpublish: mockUnpublish, - onIdle: jest.fn() + onIdle: mockUnpublishOnIdle }; }) }; }); -const mockClient = { - contentItems: { - get: jest.fn() - }, - hubs: { - get: jest.fn() - } -} as unknown as DynamicContent; - -const mockLog = { - open: jest.fn().mockReturnValue({ - appendLine: jest.fn(), - addComment: jest.fn(), - close: jest.fn() - }) -} as unknown as FileLog; - describe('unpublish tests', () => { describe('builder tests', () => { it('should configure yargs', function () { @@ -97,171 +90,196 @@ describe('unpublish tests', () => { }); }); - describe('getContentItems tests', () => { - beforeEach(() => jest.clearAllMocks()); - - it('should return content items by id', async () => { - const mockItem = { id: '1', status: Status.ACTIVE } as ContentItem; - mockClient.contentItems.get = jest.fn().mockResolvedValue(mockItem); - - const result = await getContentItems({ - client: mockClient, - id: '1', - hubId: 'hub-id' - }); - - expect(result.contentItems).toEqual([mockItem]); - expect(result.missingContent).toBe(false); - }); - - it('should filter out non-active content items', async () => { - mockClient.contentItems.get = jest - .fn() - .mockResolvedValueOnce({ id: '1', status: Status.ARCHIVED }) - .mockResolvedValueOnce({ id: '2', status: Status.ACTIVE }); - - const result = await getContentItems({ - client: mockClient, - id: ['1', '2'], - hubId: 'hub-id' - }); - - expect(result.contentItems).toHaveLength(1); - expect(result.contentItems[0].id).toBe('2'); - expect(result.missingContent).toBe(true); - }); - - it('should return content using fallback filters', async () => { - const mockHub = {} as Hub; - const contentItems = [{ id: 'a', status: Status.ACTIVE }] as ContentItem[]; - const getContent = require('../../common/filter/fetch-content').getContent; - mockClient.hubs.get = jest.fn().mockResolvedValue(mockHub); - getContent.mockResolvedValue(contentItems); + describe('handler', () => { + const HUB_ID = '67d1c1c7642fa239dbe15164'; + const CONTENT_ITEM_ID = 'c5b659df-680e-4711-bfbe-84eaa10d76cc'; + const globalArgs = { + $0: 'test', + _: ['test'], + json: true, + clientId: 'client-id', + clientSecret: 'client-secret', + hubId: HUB_ID + }; + const mockAppendLine = jest.fn(); + const mockLog = { + open: jest.fn().mockReturnValue({ + appendLine: mockAppendLine, + addComment: jest.fn(), + close: jest.fn() + }) + } as unknown as FileLog; - const result = await getContentItems({ - client: mockClient, - hubId: 'hub-id', - facet: 'label:test' - }); - - expect(result.contentItems).toEqual(contentItems); - }); - }); - - describe('processItems tests', () => { beforeEach(() => { jest.clearAllMocks(); - jest.mock('readline'); + confirmAllContentSpy.mockResolvedValue(true); + asyncQuestionSpy.mockResolvedValue(true); }); - it('should exit early if no content items', async () => { - console.log = jest.fn(); - - await processItems({ - contentItems: [], - logFile: mockLog, - allContent: false, - missingContent: false + it('should publish content item by id', async () => { + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: CONTENT_ITEM_ID, body: { _meta: {} } }) + ]); - expect(console.log).toHaveBeenCalledWith('Nothing found to unpublish, aborting.'); - }); - - it('should confirm before unpublishing when force is false', async () => { - const confirmAllContent = require('../../common/content-item/confirm-all-content').confirmAllContent; - confirmAllContent.mockResolvedValue(false); - console.log = jest.fn(); + mockUnpublish.mockImplementation((contentItem, fn) => { + fn(new Job({ id: '68e5289f0aba3024bde050f9', status: 'COMPLETE' })); + }); - await processItems({ - contentItems: [new ContentItem({ id: '1', label: 'Test', body: { _meta: {} } })], - force: false, - silent: true, - logFile: mockLog, - allContent: false, - missingContent: false + await handler({ + ...globalArgs, + id: CONTENT_ITEM_ID, + logFile: mockLog }); - expect(confirmAllContent).toHaveBeenCalled(); + expect(getContentByIds).toHaveBeenCalledWith(expect.any(Object), [CONTENT_ITEM_ID]); + expect(mockUnpublish).toHaveBeenCalledTimes(1); + expect(mockUnpublish).toHaveBeenCalledWith(expect.any(ContentItem), expect.any(Function)); + expect(mockUnpublishOnIdle).toHaveBeenCalledTimes(1); }); + it('should publish content items by query', async () => { + const REPOSITORY_ID = '67d1c1cf642fa239dbe15165'; + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContent as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: CONTENT_ITEM_ID, body: { _meta: {} } }) + ]); - it('should process all items and call unpublish', async () => { - const contentItem = new ContentItem({ - id: '1', - label: 'Unpublish Me', - body: { _meta: {} } + mockUnpublish.mockImplementation((contentItem, fn) => { + fn(new Job({ id: '68e5289f0aba3024bde050f9', status: 'COMPLETE' })); }); - await processItems({ - contentItems: [contentItem], - force: true, - silent: true, - logFile: mockLog, - allContent: false, - missingContent: false + await handler({ + ...globalArgs, + repoId: REPOSITORY_ID, + logFile: mockLog }); + expect(getContent).toHaveBeenCalledWith(expect.any(Object), expect.any(Hub), undefined, { + enrichItems: true, + folderId: undefined, + repoId: REPOSITORY_ID, + status: 'ACTIVE' + }); expect(mockUnpublish).toHaveBeenCalledTimes(1); + expect(mockUnpublish).toHaveBeenCalledWith(expect.any(ContentItem), expect.any(Function)); + expect(mockUnpublishOnIdle).toHaveBeenCalledTimes(1); }); - it('should process all items while filtering out any dependencies and call unpublish', async () => { - const contentItemDependency = new ContentItem({ + it('should process only process content items with an unpublishable status', async () => { + const publishedContentItem = new ContentItem({ + id: 'da2ee918-34c3-4fc1-ae05-111111111111', + label: 'Published - unpublish me', + publishingStatus: ContentItemPublishingStatus.LATEST, + body: { + _meta: {}, + text: 'text 1' + } + }); + const unpublishedContentItemDependency = new ContentItem({ id: 'da2ee918-34c3-4fc1-ae05-222222222222', - label: 'No need to unpublish me', - body: { _meta: {} } + label: 'Already unpublished - ignore me', + publishingStatus: ContentItemPublishingStatus.UNPUBLISHED, + body: { + _meta: {}, + text: 'text 1' + } }); - - const contentItemWithDependency = new ContentItem({ - id: 'da2ee918-34c3-4fc1-ae05-111111111111', - label: 'Unpublish me', + const notPublishedContentItemDependency = new ContentItem({ + id: 'da2ee918-34c3-4fc1-ae05-333333333333', + label: 'Never been published - ignore me', + publishingStatus: ContentItemPublishingStatus.NONE, body: { _meta: {}, - dependency: contentItemDependency + text: 'text 1' } }); - await processItems({ - contentItems: [contentItemWithDependency], - force: true, - silent: true, - logFile: mockLog, - allContent: false, - missingContent: false + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([ + publishedContentItem, + unpublishedContentItemDependency, + notPublishedContentItemDependency + ]); + + mockUnpublish.mockImplementation((contentItem, fn) => { + fn(new ContentItem({ id: '68e5289f0aba3024bde050f9' })); + }); + + await handler({ + ...globalArgs, + id: [publishedContentItem.id, unpublishedContentItemDependency.id, notPublishedContentItemDependency.id], + logFile: mockLog }); expect(mockUnpublish).toHaveBeenCalledTimes(1); + expect(mockUnpublish).toHaveBeenCalledWith(publishedContentItem, expect.any(Function)); + expect(mockUnpublishOnIdle).toHaveBeenCalledTimes(1); }); - }); - describe('handler tests', () => { - const clientFactory = require('../../services/dynamic-content-client-factory').default; - const getItemsSpy = jest.spyOn(require('./unpublish'), 'getContentItems'); - const processSpy = jest.spyOn(require('./unpublish'), 'processItems'); - beforeEach(() => { - jest.clearAllMocks(); - clientFactory.mockReturnValue(mockClient); - getItemsSpy.mockResolvedValue({ - contentItems: [{ id: '123', label: 'Test', status: Status.ACTIVE }], - missingContent: false + it('should exit before processing content items if confirmation to proceed is rejected', async () => { + confirmAllContentSpy.mockResolvedValue(false); + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([ + new ContentItem({ id: CONTENT_ITEM_ID, body: { _meta: {} } }) + ]); + + await handler({ + ...globalArgs, + id: CONTENT_ITEM_ID, + logFile: mockLog }); - processSpy.mockResolvedValue(undefined); + expect(mockUnpublish).toHaveBeenCalledTimes(0); + expect(mockUnpublishOnIdle).toHaveBeenCalledTimes(0); }); - it('should warn when both id and facet are provided', async () => { - console.log = jest.fn(); + + it('should exit early if ID or query args are not passed', async () => { await handler({ - id: '1', - facet: 'label:test', - hubId: 'hub-id', + ...globalArgs, + id: CONTENT_ITEM_ID, + facet: 'mock-facet', logFile: mockLog - } as Arguments); - expect(console.log).toHaveBeenCalledWith('Please specify either a facet or an ID - not both.'); + }); + expect(mockAppendLine).toHaveBeenCalledWith('Please specify either a facet or an ID - not both'); + expect(mockUnpublish).toHaveBeenCalledTimes(0); + expect(mockUnpublishOnIdle).toHaveBeenCalledTimes(0); }); - it('should process items with valid inputs', async () => { + + it('should exit early if no content items', async () => { + const mockGetHub = jest.fn(); + (dynamicContentClientFactory as jest.Mock).mockReturnValue({ + hubs: { + get: mockGetHub.mockResolvedValue(new Hub({ id: HUB_ID })) + } + }); + (getContentByIds as unknown as jest.Mock).mockResolvedValue([]); await handler({ - hubId: 'hub-id', + ...globalArgs, + id: CONTENT_ITEM_ID, logFile: mockLog - } as Arguments); - expect(getItemsSpy).toHaveBeenCalled(); - expect(processSpy).toHaveBeenCalled(); + }); + expect(mockAppendLine).toHaveBeenCalledWith('Nothing found to unpublish, aborting'); + expect(mockUnpublish).toHaveBeenCalledTimes(0); + expect(mockUnpublishOnIdle).toHaveBeenCalledTimes(0); }); }); }); diff --git a/src/commands/content-item/unpublish.ts b/src/commands/content-item/unpublish.ts index 35824bd8..e02cf89a 100644 --- a/src/commands/content-item/unpublish.ts +++ b/src/commands/content-item/unpublish.ts @@ -1,16 +1,16 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { Arguments, Argv } from 'yargs'; import { ConfigurationParameters } from '../configure'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { confirmAllContent } from '../../common/content-item/confirm-all-content'; import PublishOptions from '../../common/publish/publish-options'; -import { ContentItem, ContentItemPublishingStatus, DynamicContent, Status } from 'dc-management-sdk-js'; +import { ContentItem, ContentItemPublishingStatus, Status } from 'dc-management-sdk-js'; import { getDefaultLogPath, createLog } from '../../common/log-helpers'; import { FileLog } from '../../common/file-log'; import { withOldFilters } from '../../common/filter/facet'; import { getContent } from '../../common/filter/fetch-content'; import { progressBar } from '../../common/progress-bar/progress-bar'; import { ContentItemUnpublishingService } from '../../common/publishing/content-item-unpublishing-service'; +import { getContentByIds } from '../../common/content-item/get-content-items-by-ids'; export const command = 'unpublish [id]'; @@ -67,106 +67,20 @@ export const builder = (yargs: Argv): void => { }); }; -export const getContentItems = async ({ - client, - id, - hubId, - repoId, - folderId, - facet -}: { - client: DynamicContent; - id?: string | string[]; - hubId: string; - repoId?: string | string[]; - folderId?: string | string[]; - facet?: string; -}): Promise<{ contentItems: ContentItem[]; missingContent: boolean }> => { - try { - let contentItems: ContentItem[] = []; - - if (id != null) { - const itemIds = Array.isArray(id) ? id : [id]; - const items: ContentItem[] = []; - - for (const id of itemIds) { - try { - items.push(await client.contentItems.get(id)); - } catch { - // Missing item. - } - } - - contentItems.push(...items.filter(item => item.status === Status.ACTIVE)); - - return { - contentItems, - missingContent: contentItems.length != itemIds.length - }; - } - - const hub = await client.hubs.get(hubId); - - contentItems = await getContent(client, hub, facet, { repoId, folderId, status: Status.ACTIVE, enrichItems: true }); - - return { contentItems, missingContent: false }; - } catch (err) { - console.log(err); - - return { - contentItems: [], - missingContent: false - }; - } -}; - export const processItems = async ({ contentItems, - force, - silent, - logFile, - allContent, - missingContent + log }: { contentItems: ContentItem[]; - force?: boolean; - silent?: boolean; - logFile: FileLog; - allContent: boolean; - missingContent: boolean; + log: FileLog; }): Promise => { - if (contentItems.length == 0) { - console.log('Nothing found to unpublish, aborting.'); - return; - } - - const rootContentPublishedItems = contentItems.filter( - item => - item.publishingStatus !== ContentItemPublishingStatus.UNPUBLISHED && - item.publishingStatus !== ContentItemPublishingStatus.NONE - ); - - const log = logFile.open(); - log.appendLine(`Found ${rootContentPublishedItems.length} items to unpublish.`); - - if (rootContentPublishedItems.length === 0) { - return; - } - - if (!force) { - const yes = await confirmAllContent('unpublish', 'content items', allContent, missingContent); - if (!yes) { - return; - } - } - - log.appendLine(`Unpublishing ${rootContentPublishedItems.length} items.`); + log.appendLine(`Unpublishing ${contentItems.length} items.`); const unpublishingService = new ContentItemUnpublishingService(); const contentItemUnpublishJobs: ContentItem[] = []; - const unpublishProgress = progressBar(rootContentPublishedItems.length, 0, { title: 'Unpublishing content items' }); + const unpublishProgress = progressBar(contentItems.length, 0, { title: 'Unpublishing content items' }); - for (const item of rootContentPublishedItems) { + for (const item of contentItems) { try { await unpublishingService.unpublish(item, contentItem => { contentItemUnpublishJobs.push(contentItem); @@ -182,52 +96,72 @@ export const processItems = async ({ await unpublishingService.onIdle(); unpublishProgress.stop(); - - log.appendLine(`The request for content item/s to be unpublished has been completed - please manually verify.`); - - await log.close(!silent); }; export const handler = async (argv: Arguments): Promise => { const { id, logFile, force, silent, hubId, repoId, folderId } = argv; + const log = logFile.open(); const client = dynamicContentClientFactory(argv); - const facet = withOldFilters(argv.facet, argv); - const allContent = !id && !facet && !folderId && !repoId; if (repoId && id) { - console.log('ID of content item is specified, ignoring repository ID'); + log.appendLine('ID of content item is specified, ignoring repository ID'); } if (id && facet) { - console.log('Please specify either a facet or an ID - not both.'); + log.appendLine('Please specify either a facet or an ID - not both'); return; } if (repoId && folderId) { - console.log('Folder is specified, ignoring repository ID'); + log.appendLine('Folder is specified, ignoring repository ID'); } if (allContent) { - console.log('No filter was given, unpublishing all content'); + log.appendLine('No filter was given, unpublishing all content'); } - const { contentItems, missingContent } = await getContentItems({ - client, - id, - hubId, - repoId, - folderId, - facet - }); + let ids: string[] = []; + + if (id) { + ids = Array.isArray(id) ? id : [id]; + } + + const hub = await client.hubs.get(hubId); + const contentItems = + ids.length > 0 + ? await getContentByIds(client, ids) + : await getContent(client, hub, facet, { repoId, folderId, status: Status.ACTIVE, enrichItems: true }); + + const unpublishableContentItems = contentItems.filter( + item => + item.publishingStatus !== ContentItemPublishingStatus.UNPUBLISHED && + item.publishingStatus !== ContentItemPublishingStatus.NONE + ); + + if (!unpublishableContentItems.length) { + log.appendLine('Nothing found to unpublish, aborting'); + return; + } + + const missingContentItems = ids.length > 0 ? Boolean(ids.length !== unpublishableContentItems.length) : false; + + log.appendLine(`Found ${unpublishableContentItems.length} content items to unpublish\n`); + + if (!force) { + const yes = await confirmAllContent('unpublish', 'content items', allContent, missingContentItems); + if (!yes) { + return; + } + } await processItems({ - contentItems, - force, - silent, - logFile, - allContent, - missingContent + contentItems: unpublishableContentItems, + log }); + + log.appendLine(`Unpublish complete - please manually verify unpublish status`); + + await log.close(!silent); }; diff --git a/src/common/archive/content-item-unarchive-options.ts b/src/common/archive/content-item-unarchive-options.ts new file mode 100644 index 00000000..f42f9ef4 --- /dev/null +++ b/src/common/archive/content-item-unarchive-options.ts @@ -0,0 +1,15 @@ +import { FileLog } from '../file-log'; + +export default interface ContentItemUnarchiveOptions { + id?: string; + schemaId?: string | string[]; + repoId?: string | string[]; + folderId?: string | string[]; + revertLog?: string; + facet?: string; + logFile: FileLog; + force?: boolean; + silent?: boolean; + ignoreError?: boolean; + ignoreSchemaValidation?: boolean; +} diff --git a/src/common/burstable-queue/burstable-queue.spec.ts b/src/common/burstable-queue/burstable-queue.spec.ts index ba2bfbe6..6802cdf8 100644 --- a/src/common/burstable-queue/burstable-queue.spec.ts +++ b/src/common/burstable-queue/burstable-queue.spec.ts @@ -4,6 +4,7 @@ import { setTimeout } from 'node:timers/promises'; describe('burstable-queue', () => { it('should schedule task and execute them with an initial burst', async () => { const interval = 500; + const timeoutInterval = interval + 10; const burstableQueue = new BurstableQueue({ concurrency: 1, minTime: 0, @@ -23,19 +24,19 @@ describe('burstable-queue', () => { expect(burstableQueue.size()).toEqual(8); expect(completeTasks).toHaveLength(0); - await setTimeout(interval); + await setTimeout(timeoutInterval); expect(burstableQueue.size()).toEqual(4); expect(completeTasks).toHaveLength(4); - await setTimeout(interval); + await setTimeout(timeoutInterval); expect(burstableQueue.size()).toEqual(3); expect(completeTasks).toHaveLength(5); - await setTimeout(interval); + await setTimeout(timeoutInterval); expect(burstableQueue.size()).toEqual(2); expect(completeTasks).toHaveLength(6); - await setTimeout(interval); + await setTimeout(timeoutInterval); expect(burstableQueue.size()).toEqual(1); expect(completeTasks).toHaveLength(7); - await setTimeout(interval); + await setTimeout(timeoutInterval); expect(burstableQueue.size()).toEqual(0); expect(completeTasks).toHaveLength(8); });