diff --git a/src/commands/event/archive.spec.ts b/src/commands/event/archive.spec.ts index b4fc336a..a66158c2 100644 --- a/src/commands/event/archive.spec.ts +++ b/src/commands/event/archive.spec.ts @@ -1,4 +1,5 @@ -import { builder, command, handler, LOG_FILENAME, getEvents, coerceLog } from './archive'; +import { builder, command, handler, LOG_FILENAME, getEvents, coerceLog, filterEvents, filterEditions } from './archive'; +import * as archive from './archive'; import dynamicContentClientFactory from '../../services/dynamic-content-client-factory'; import { Event, Edition, Hub } from 'dc-management-sdk-js'; import Yargs from 'yargs/yargs'; @@ -45,7 +46,8 @@ describe('event archive command', () => { expect(spyPositional).toHaveBeenCalledWith('id', { type: 'string', - describe: 'The ID of an Event to be archived. If id is not provided, this command will not archive something.' + describe: + 'The ID of an Event to be archived. If id is not provided, this command will not archive something. Ignores other filters, and archives regardless of whether the event is active or not.' }); expect(spyOption).toHaveBeenCalledWith('name', { @@ -66,6 +68,29 @@ describe('event archive command', () => { describe: 'If present, no log file will be produced.' }); + expect(spyOption).toHaveBeenCalledWith('editions', { + type: 'boolean', + boolean: true, + describe: 'Only archive and delete editions, not events.' + }); + + expect(spyOption).toHaveBeenCalledWith('onlyInactive', { + type: 'boolean', + boolean: true, + describe: 'Only archive and delete inactive editons and events.' + }); + + expect(spyOption).toHaveBeenCalledWith('fromDate', { + describe: + 'Start date for filtering events. Either "NOW" or in the format ":", example: "-7:DAYS".', + type: 'string' + }); + + expect(spyOption).toHaveBeenCalledWith('toDate', { + describe: 'To date for filtering events. Either "NOW" or in the format ":", example: "-7:DAYS".', + type: 'string' + }); + expect(spyOption).toHaveBeenCalledWith('logFile', { type: 'string', default: LOG_FILENAME, @@ -88,7 +113,9 @@ describe('event archive command', () => { deleteResource = false, mixedEditions = false, getHubError = false, - getEventError = false + getEventError = false, + start = '', + end = '' }): { mockGet: () => void; mockEditionsList: () => void; @@ -149,6 +176,8 @@ describe('event archive command', () => { new Event({ id: 'test1', name: 'test1', + start, + end, client: { fetchLinkedResource: mockEditionsList, performActionThatReturnsResource: archiveMock, @@ -177,6 +206,8 @@ describe('event archive command', () => { new Event({ id: 'test2', name: 'test2', + start, + end, client: { fetchLinkedResource: mockEditionsList, performActionThatReturnsResource: archiveMock, @@ -209,6 +240,8 @@ describe('event archive command', () => { new Event({ name: 'test1', id: '1', + start, + end, client: { fetchLinkedResource: mockEditionsList, performActionThatReturnsResource: archiveMock, @@ -241,6 +274,8 @@ describe('event archive command', () => { name: 'ed1', id: 'ed1', publishingStatus: status, + start, + end, client: { fetchLinkedResource: mockEditionsList, performActionThatReturnsResource: archiveMock, @@ -270,6 +305,8 @@ describe('event archive command', () => { name: 'ed2', id: 'ed2', publishingStatus: 'PUBLISHED', + start, + end, client: { fetchLinkedResource: mockEventsList, performActionThatReturnsResource: archiveMock, @@ -504,6 +541,30 @@ describe('event archive command', () => { expect(archiveMock).toHaveBeenCalledTimes(2); }); + it('should delete 1 edition without archiving events if editions option is given', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (readline as any).setResponses(['y']); + + const { mockEditionsList, deleteMock, archiveMock, mockGet } = mockValues({ + status: 'DRAFT', + mixedEditions: true + }); + + const argv = { + ...yargArgs, + ...config, + id: '1', + name: 'test', + editions: true + }; + await handler(argv); + + expect(mockGet).toHaveBeenCalled(); + expect(mockEditionsList).toHaveBeenCalled(); + expect(deleteMock).toHaveBeenCalledTimes(1); + expect(archiveMock).toHaveBeenCalledTimes(1); + }); + it('should answer no', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['n']); @@ -621,6 +682,10 @@ describe('event archive command', () => { }); describe('getEvents tests', () => { + beforeEach(() => { + mockValues({}); + }); + it('should get event by id', async () => { const result = await getEvents({ client: dynamicContentClientFactory({ @@ -653,6 +718,221 @@ describe('event archive command', () => { } }); + it('should filter events by from and to date', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(input => input); + + const from = new Date(); + const to = new Date(); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + from, + to + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), from, to); + expect(filterEditionSpy).not.toHaveBeenCalled(); + + if (result) { + expect(result.length).toBe(2); + } + }); + + it('should filter events by name and date at the same time', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + + const from = new Date(); + const to = new Date(); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + name: '/test1/', + from, + to + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), from, to); + + if (result) { + expect(result.length).toBe(1); + } + }); + + it('should filter editions by from and to date if editions argument passed', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(input => input); + + const from = new Date(); + const to = new Date(); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + from, + to, + editions: true + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), from, to); + expect(filterEditionSpy).toHaveBeenCalledWith(expect.any(Array), from, to); + + if (result) { + expect(result.length).toBe(2); + } + }); + + it('should filter out active events if onlyInactive is true', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(input => input); + + const from = new Date(); + from.setDate(from.getDate() - 1); + const to = new Date(); + to.setDate(to.getDate() + 1); + + mockValues({ start: from.toISOString(), end: to.toISOString() }); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + onlyInactive: true + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), undefined, undefined); + expect(filterEditionSpy).not.toHaveBeenCalled(); + + if (result) { + expect(result.length).toBe(0); + } + }); + + it('should not filter out inactive events if onlyInactive is true', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(input => input); + + const from = new Date(); + from.setDate(from.getDate() - 2); + const to = new Date(); + to.setDate(to.getDate() - 1); + + mockValues({ start: from.toISOString(), end: to.toISOString() }); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + onlyInactive: true + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), undefined, undefined); + expect(filterEditionSpy).not.toHaveBeenCalled(); + + if (result) { + expect(result.length).toBe(2); + } + }); + + it('should filter out active editions if onlyInactive and editions are true', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(input => input); + + const from = new Date(); + from.setDate(from.getDate() - 1); + const to = new Date(); + to.setDate(to.getDate() + 1); + + mockValues({ start: from.toISOString(), end: to.toISOString() }); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + onlyInactive: true, + editions: true + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), undefined, undefined); + expect(filterEditionSpy).toHaveBeenCalledWith(expect.any(Array), undefined, undefined); + + if (result) { + expect(result.length).toBe(0); + } + }); + + it('should not filter out inactive editions if onlyInactive and editions are true', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(input => input); + + const from = new Date(); + from.setDate(from.getDate() - 2); + const to = new Date(); + to.setDate(to.getDate() - 1); + + mockValues({ start: from.toISOString(), end: to.toISOString() }); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + onlyInactive: true, + editions: true + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), undefined, undefined); + expect(filterEditionSpy).toHaveBeenCalledWith(expect.any(Array), undefined, undefined); + + if (result) { + expect(result.length).toBe(2); + } + }); + + it('should not return events with no editions if editions argument is provided', async () => { + const filterEventSpy = jest.spyOn(archive, 'filterEvents').mockImplementation(input => input); + const filterEditionSpy = jest.spyOn(archive, 'filterEditions').mockImplementation(() => []); + + const from = new Date(); + const to = new Date(); + + const result = await getEvents({ + client: dynamicContentClientFactory({ + ...config, + ...yargArgs + }), + hubId: 'hub1', + from, + to, + editions: true + }); + + expect(filterEventSpy).toHaveBeenCalledWith(expect.any(Array), from, to); + expect(filterEditionSpy).toHaveBeenCalledWith(expect.any(Array), from, to); + + if (result) { + expect(result.length).toBe(0); + } + }); + it('should archive events, write log file', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); @@ -696,4 +976,68 @@ describe('event archive command', () => { await promisify(unlink)(`temp_${process.env.JEST_WORKER_ID}/event-archive.log`); }); }); + + describe('filterEvents tests', () => { + const testEvents = [ + new Event({ start: '2021-01-01T12:00:00.000Z', end: '2021-05-05T12:00:00.000Z' }), + new Event({ start: '2021-04-04T12:00:00.000Z', end: '2021-06-06T12:00:00.000Z' }), + new Event({ start: '2021-08-08T12:00:00.000Z', end: '2021-09-09T12:00:00.000Z' }), + new Event({ start: '2021-01-01T12:00:00.000Z', end: '2021-10-10T12:00:00.000Z' }) + ]; + + it('should return the input events if from and to are undefined', async () => { + expect(filterEvents(testEvents, undefined, undefined)).toEqual(testEvents); + }); + + it('should filter out events from before the from date when provided', async () => { + expect(filterEvents(testEvents, new Date('2021-08-08T12:00:00.000Z'), undefined)).toEqual(testEvents.slice(2)); + }); + + it('should filter out events from after the to date when provided', async () => { + expect(filterEvents(testEvents, undefined, new Date('2021-07-07T12:00:00.000Z'))).toEqual([ + testEvents[0], + testEvents[1], + testEvents[3] + ]); + }); + + it('should filter out events outwith the from and to dates when both are provided', async () => { + expect( + filterEvents(testEvents, new Date('2021-05-06T12:00:00.000Z'), new Date('2021-07-07T12:00:00.000Z')) + ).toEqual([testEvents[1], testEvents[3]]); + }); + }); + + describe('filterEditions tests', () => { + const testEditions = [ + new Edition({ start: '2021-01-01T12:00:00.000Z', end: '2021-05-05T12:00:00.000Z' }), + new Edition({ start: '2021-04-04T12:00:00.000Z', end: '2021-06-06T12:00:00.000Z' }), + new Edition({ start: '2021-08-08T12:00:00.000Z', end: '2021-09-09T12:00:00.000Z' }), + new Edition({ start: '2021-01-01T12:00:00.000Z', end: '2021-10-10T12:00:00.000Z' }) + ]; + + it('should return the input editions if from and to are undefined', async () => { + expect(filterEditions(testEditions, undefined, undefined)).toEqual(testEditions); + }); + + it('should filter out editions from before the from date when provided', async () => { + expect(filterEditions(testEditions, new Date('2021-08-08T12:00:00.000Z'), undefined)).toEqual( + testEditions.slice(2) + ); + }); + + it('should filter out editions from after the to date when provided', async () => { + expect(filterEditions(testEditions, undefined, new Date('2021-07-07T12:00:00.000Z'))).toEqual([ + testEditions[0], + testEditions[1], + testEditions[3] + ]); + }); + + it('should filter out editions outwith the from and to dates when both are provided', async () => { + expect( + filterEditions(testEditions, new Date('2021-05-06T12:00:00.000Z'), new Date('2021-07-07T12:00:00.000Z')) + ).toEqual([testEditions[1], testEditions[3]]); + }); + }); }); diff --git a/src/commands/event/archive.ts b/src/commands/event/archive.ts index 9f8e05d9..ad357427 100644 --- a/src/commands/event/archive.ts +++ b/src/commands/event/archive.ts @@ -8,6 +8,7 @@ import { Edition, Event, DynamicContent } from 'dc-management-sdk-js'; import { equalsOrRegex } from '../../common/filter/filter'; import { createLog, getDefaultLogPath } from '../../common/log-helpers'; import { FileLog } from '../../common/file-log'; +import { relativeDate } from '../../common/filter/facet'; const maxAttempts = 30; export const command = 'archive [id]'; @@ -23,7 +24,8 @@ export const builder = (yargs: Argv): void => { yargs .positional('id', { type: 'string', - describe: 'The ID of an Event to be archived. If id is not provided, this command will not archive something.' + describe: + 'The ID of an Event to be archived. If id is not provided, this command will not archive something. Ignores other filters, and archives regardless of whether the event is active or not.' }) .option('name', { type: 'string', @@ -42,6 +44,24 @@ export const builder = (yargs: Argv): void => { boolean: true, describe: 'If present, no log file will be produced.' }) + .option('editions', { + type: 'boolean', + boolean: true, + describe: 'Only archive and delete editions, not events.' + }) + .option('onlyInactive', { + type: 'boolean', + boolean: true, + describe: 'Only archive and delete inactive editons and events.' + }) + .option('fromDate', { + describe: 'Start date for filtering events. Either "NOW" or in the format ":", example: "-7:DAYS".', + type: 'string' + }) + .option('toDate', { + describe: 'To date for filtering events. Either "NOW" or in the format ":", example: "-7:DAYS".', + type: 'string' + }) .option('logFile', { type: 'string', default: LOG_FILENAME, @@ -50,6 +70,40 @@ export const builder = (yargs: Argv): void => { }); }; +export const filterEvents = (events: Event[], from: Date | undefined, to: Date | undefined): Event[] => { + return events.filter(event => { + const eventStart = new Date(event.start as string); + const eventEnd = new Date(event.end as string); + + if (from && eventEnd < from) { + return false; + } + + if (to && eventStart > to) { + return false; + } + + return true; + }); +}; + +export const filterEditions = (editions: Edition[], from: Date | undefined, to: Date | undefined): Edition[] => { + return editions.filter(edition => { + const editionStart = new Date(edition.start as string); + const editionEnd = new Date(edition.end as string); + + if (from && editionEnd < from) { + return false; + } + + if (to && editionStart > to) { + return false; + } + + return true; + }); +}; + const getEventUntilSuccess = async ({ id = '', resource = 'archive', @@ -74,38 +128,82 @@ const getEventUntilSuccess = async ({ return resourceEvent; }; +type EventEntry = { + event: Event; + editions: Edition[]; + archiveEditions: Edition[]; + deleteEditions: Edition[]; + unscheduleEditions: Edition[]; + command: string; +}; + +export const preprocessEvents = (entries: EventEntry[], editions?: boolean, onlyInactive?: boolean): EventEntry[] => { + if (onlyInactive) { + const now = new Date(); + + entries = entries.filter(entry => { + if (editions) { + // Archive editions if the current date doesn't fall within the edition date. + entry.editions = entry.editions.filter(edition => { + const start = new Date(edition.start as string); + const end = new Date(edition.end as string); + + return now < start || now > end; + }); + + return entry.editions.length != 0; + } else { + // Only archive an event if the current date doesn't fall within the event date. + const start = new Date(entry.event.start as string); + const end = new Date(entry.event.end as string); + + return now < start || now > end; + } + }); + } + + if (editions) { + entries = entries.filter(entry => entry.editions.length != 0); + } + + return entries; +}; + export const getEvents = async ({ id, client, hubId, - name + name, + from, + to, + editions, + onlyInactive }: { id?: string | string[]; hubId: string; name?: string | string[]; + from?: Date; + to?: Date; + editions?: boolean; + onlyInactive?: boolean; client: DynamicContent; -}): Promise< - { - event: Event; - editions: Edition[]; - archiveEditions: Edition[]; - deleteEditions: Edition[]; - unscheduleEditions: Edition[]; - command: string; - }[] -> => { +}): Promise => { try { if (id != null) { const ids = Array.isArray(id) ? id : [id]; - return await Promise.all( + const result = await Promise.all( ids.map(async id => { const event = await client.events.get(id); - const editions = await paginator(event.related.editions.list); + let foundEditions = await paginator(event.related.editions.list); + + if (editions) { + foundEditions = filterEditions(foundEditions, from, to); + } return { event, - editions, + editions: foundEditions, command: 'ARCHIVE', unscheduleEditions: [], deleteEditions: [], @@ -113,10 +211,12 @@ export const getEvents = async ({ }; }) ); + + return preprocessEvents(result, editions, onlyInactive); } const hub = await client.hubs.get(hubId); - const eventsList = await paginator(hub.related.events.list); + const eventsList = filterEvents(await paginator(hub.related.events.list), from, to); let events: Event[] = eventsList; if (name != null) { @@ -129,16 +229,26 @@ export const getEvents = async ({ ); } - return await Promise.all( - events.map(async event => ({ - event, - editions: await paginator(event.related.editions.list), - command: 'ARCHIVE', - unscheduleEditions: [], - deleteEditions: [], - archiveEditions: [] - })) + const result = await Promise.all( + events.map(async event => { + let foundEditions = await paginator(event.related.editions.list); + + if (editions) { + foundEditions = filterEditions(foundEditions, from, to); + } + + return { + event, + editions: foundEditions, + command: 'ARCHIVE', + unscheduleEditions: [], + deleteEditions: [], + archiveEditions: [] + }; + }) ); + + return preprocessEvents(result, editions, onlyInactive); } catch (e) { console.log(e); return []; @@ -151,7 +261,8 @@ export const processItems = async ({ force, silent, missingContent, - logFile + logFile, + editions }: { client: DynamicContent; events: { @@ -167,6 +278,7 @@ export const processItems = async ({ logFile: FileLog; missingContent: boolean; ignoreError?: boolean; + editions?: boolean; }): Promise => { try { for (let i = 0; i < events.length; i++) { @@ -187,8 +299,15 @@ export const processItems = async ({ console.log('The following events are processing:'); events.forEach(({ event, command = '', deleteEditions, unscheduleEditions, archiveEditions }) => { - console.log(`${command}: ${event.name} (${event.id})`); - if (deleteEditions.length || unscheduleEditions.length) { + const hasEditions = deleteEditions.length || unscheduleEditions.length; + + if (!editions) { + console.log(`${command}: ${event.name} (${event.id})`); + } else if (hasEditions) { + console.log(`${event.name} (${event.id})`); + } + + if (hasEditions) { console.log(' Editions:'); deleteEditions.forEach(({ name, id }) => { console.log(` DELETE: ${name} (${id})`); @@ -213,36 +332,43 @@ export const processItems = async ({ const log = logFile.open(); let successCount = 0; + let editionSuccessCount = 0; for (let i = 0; i < events.length; i++) { try { await Promise.all(events[i].unscheduleEditions.map(edition => edition.related.unschedule())); - if (events[i].command === 'ARCHIVE') { - await Promise.all(events[i].deleteEditions.map(edition => edition.related.delete())); + if (events[i].command === 'ARCHIVE' || editions) { + await Promise.all( + events[i].deleteEditions.map(edition => edition.related.delete().then(() => editionSuccessCount++)) + ); - await Promise.all(events[i].archiveEditions.map(edition => edition.related.archive())); + await Promise.all( + events[i].archiveEditions.map(edition => edition.related.archive().then(() => editionSuccessCount++)) + ); } - const resource = await getEventUntilSuccess({ - id: events[i].event.id || '', - resource: events[i].command.toLowerCase(), - client - }); - - if (!resource) { - log.addComment(`${events[i].command} FAILED: ${events[i].event.id}`); - log.addComment(`You don't have access to perform this action, try again later or contact support.`); - } - - if (events[i].command === 'DELETE') { - resource && (await resource.related.delete()); - log.addAction(events[i].command, `${events[i].event.id}\n`); - successCount++; - } else { - resource && (await resource.related.archive()); - log.addAction(events[i].command, `${events[i].event.id}\n`); - successCount++; + if (!editions) { + const resource = await getEventUntilSuccess({ + id: events[i].event.id || '', + resource: events[i].command.toLowerCase(), + client + }); + + if (!resource) { + log.addComment(`${events[i].command} FAILED: ${events[i].event.id}`); + log.addComment(`You don't have access to perform this action, try again later or contact support.`); + } + + if (events[i].command === 'DELETE') { + resource && (await resource.related.delete()); + log.addAction(events[i].command, `${events[i].event.id}\n`); + successCount++; + } else { + resource && (await resource.related.archive()); + log.addAction(events[i].command, `${events[i].event.id}\n`); + successCount++; + } } } catch (e) { console.log(e); @@ -253,24 +379,27 @@ export const processItems = async ({ await log.close(!silent); - return console.log(`Processed ${successCount} events.`); + return console.log(`Processed ${successCount} events, ${editionSuccessCount} editions.`); } catch (e) { return; } }; export const handler = async (argv: Arguments): Promise => { - const { id, logFile, force, silent, name, hubId } = argv; + const { id, logFile, force, silent, name, hubId, fromDate, toDate, editions, onlyInactive } = argv; const client = dynamicContentClientFactory(argv); + const from = fromDate === undefined ? undefined : relativeDate(fromDate); + const to = toDate === undefined ? undefined : relativeDate(toDate); + const missingContent = false; if (name && id) { console.log('ID of event is specified, ignoring name'); } - if (!name && !id) { - console.log('No ID or name is specified'); + if (!name && !id && !from && !to) { + console.log('No date range, ID or name is specified'); return; } @@ -278,7 +407,11 @@ export const handler = async (argv: Arguments