diff --git a/nodes/CoveDataProtection/CoveDataProtection.node.ts b/nodes/CoveDataProtection/CoveDataProtection.node.ts index 5f5a1cc..1c725f3 100644 --- a/nodes/CoveDataProtection/CoveDataProtection.node.ts +++ b/nodes/CoveDataProtection/CoveDataProtection.node.ts @@ -86,7 +86,13 @@ export class CoveDataProtection implements INodeType { } } - return partners.sort((a, b) => a.name.localeCompare(b.name)); + partners.sort((a, b) => a.name.localeCompare(b.name)); + partners.unshift({ + name: 'All Partners (Top Level)', + value: parentPartnerId, + }); + + return partners; }, }, }; diff --git a/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/description.ts b/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/description.ts new file mode 100644 index 0000000..e358c9c --- /dev/null +++ b/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/description.ts @@ -0,0 +1,144 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const getBackupStatisticsDescription: INodeProperties[] = [ + { + displayName: 'Partner Name or ID', + name: 'partnerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPartners', + }, + displayOptions: { + show: { + resource: ['accounts'], + operation: ['getBackupStatistics'], + }, + }, + default: '', + description: 'The partner to enumerate device backup statistics for. Choose from the list, or specify an ID using an expression.', + required: true, + }, + { + displayName: 'Data Sources', + name: 'dataSources', + type: 'multiOptions', + displayOptions: { + show: { + resource: ['accounts'], + operation: ['getBackupStatistics'], + }, + }, + options: [ + { name: 'Bare Metal Restore (D17)', value: 'D17' }, + { name: 'Files and Folders (D01)', value: 'D01' }, + { name: 'Hyper-V (D14)', value: 'D14' }, + { name: 'Microsoft 365 Exchange (D19)', value: 'D19' }, + { name: 'Microsoft 365 OneDrive (D20)', value: 'D20' }, + { name: 'Microsoft 365 SharePoint (D05)', value: 'D05' }, + { name: 'Microsoft 365 Teams (D23)', value: 'D23' }, + { name: 'MySql (D15)', value: 'D15' }, + { name: 'Network Shares (D06)', value: 'D06' }, + { name: 'Oracle (D12)', value: 'D12' }, + { name: 'System State (D02)', value: 'D02' }, + { name: 'Total (D09)', value: 'D09' }, + { name: 'Virtual Disaster Recovery (D16)', value: 'D16' }, + { name: 'VMware Virtual Machines (D08)', value: 'D08' }, + { name: 'VssExchange (D04)', value: 'D04' }, + { name: 'VssMsSql (D10)', value: 'D10' }, + { name: 'VssSharePoint (D11)', value: 'D11' }, + { name: 'VssSystemState (D07)', value: 'D07' }, + ], + default: ['D01', 'D02'], + description: 'Which data sources to retrieve backup statistics for', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + displayOptions: { + show: { + resource: ['accounts'], + operation: ['getBackupStatistics'], + }, + }, + options: [ + { name: 'Last Session Status (F00)', value: 'F00' }, + { name: 'Last Session Timestamp (F15)', value: 'F15' }, + { name: 'Last Successful Session Status (F16)', value: 'F16' }, + { name: 'Last Successful Session Timestamp (F09)', value: 'F09' }, + { name: 'Last Completed Session Status (F17)', value: 'F17' }, + { name: 'Last Completed Session Timestamp (F18)', value: 'F18' }, + ], + default: ['F00', 'F15'], + description: 'Which backup statistics fields to retrieve for each selected data source', + }, + { + displayName: 'Additional Options', + name: 'additionalOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['accounts'], + operation: ['getBackupStatistics'], + }, + }, + options: [ + { + displayName: 'Filter', + name: 'filter', + type: 'string', + default: '', + description: 'Filter expression (e.g., "ANY =~ "Device*"")', + }, + { + displayName: 'Selection Mode', + name: 'selectionMode', + type: 'options', + options: [ + { + name: 'Merged', + value: 'Merged', + }, + { + name: 'Detailed (Per Installation)', + value: 'PerInstallation', + }, + ], + default: 'Merged', + description: 'Selection mode for statistics', + }, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['accounts'], + operation: ['getBackupStatistics'], + }, + }, + default: true, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['accounts'], + operation: ['getBackupStatistics'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; diff --git a/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/execute.ts b/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/execute.ts new file mode 100644 index 0000000..a8a43e1 --- /dev/null +++ b/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/execute.ts @@ -0,0 +1,119 @@ +import { IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { jsonRpcRequest } from '../../../transport'; + +const SESSION_STATUS_LABELS: Record = { + '1': 'In process', + '2': 'Failed', + '3': 'Aborted', + '5': 'Completed', + '6': 'Interrupted', + '7': 'NotStarted', + '8': 'CompletedWithErrors', + '9': 'InProgressWithFaults', + '10': 'OverQuota', + '11': 'NoSelection', + '12': 'Restarted', +}; + +const FIELD_CODE_NAMES: Record = { + F00: 'lastSessionStatus', + F09: 'lastSuccessfulSessionTimestamp', + F15: 'lastSessionTimestamp', + F16: 'lastSuccessfulSessionStatus', + F17: 'lastCompletedSessionStatus', + F18: 'lastCompletedSessionTimestamp', +}; + +const STATUS_FIELD_CODES = new Set(['F00', 'F16', 'F17']); + +export async function execute(this: IExecuteFunctions, index: number): Promise { + const partnerId = this.getNodeParameter('partnerId', index) as number; + const dataSources = this.getNodeParameter('dataSources', index, []) as string[]; + const fields = this.getNodeParameter('fields', index, []) as string[]; + const additionalOptions = this.getNodeParameter('additionalOptions', index, {}) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', index, true) as boolean; + const limit = returnAll ? 0 : (this.getNodeParameter('limit', index, 50) as number); + + const dataSourceArray = dataSources.length > 0 ? dataSources : ['D01', 'D02']; + const fieldArray = fields.length > 0 ? fields : ['F00', 'F15']; + + const backupColumns = dataSourceArray.flatMap((dataSource) => + fieldArray.map((field) => `${dataSource}${field}`), + ); + + const columnArray = ['I0', 'I1', 'I18', 'I78', ...backupColumns]; + + const query: IDataObject = { + PartnerId: partnerId, + Columns: columnArray, + SelectionMode: additionalOptions.selectionMode || 'Merged', + StartRecordNumber: 0, + RecordsCount: returnAll ? 9999999 : limit, + }; + + if (additionalOptions.filter) { + query.Filter = additionalOptions.filter; + } + + const params: IDataObject = { + query, + }; + + const result = await jsonRpcRequest.call(this, 'EnumerateAccountStatistics', params); + + if (result && result.result && Array.isArray(result.result)) { + return result.result.map((item: any) => { + const normalized: IDataObject = { + accountId: item.AccountId, + partnerId: item.PartnerId, + flags: item.Flags, + backupStatistics: {}, + }; + + const backupStatistics = normalized.backupStatistics as IDataObject; + + if (item.Settings) { + item.Settings.forEach((setting: any) => { + Object.keys(setting).forEach((columnCode) => { + const value = setting[columnCode]; + + if (columnCode === 'I0') { + normalized.deviceId = value; + return; + } + if (columnCode === 'I1') { + normalized.deviceName = value; + return; + } + if (columnCode === 'I18') { + normalized.computerName = value; + return; + } + if (columnCode === 'I78') { + normalized.activeDataSources = (value as string).match(/.{1,3}/g) || []; + return; + } + + const match = columnCode.match(/^(D\d{2})(F\d{2})$/); + if (match) { + const [, dataSourceCode, fieldCode] = match; + const fieldName = FIELD_CODE_NAMES[fieldCode] || fieldCode; + + const dataSourceStats = (backupStatistics[dataSourceCode] || + (backupStatistics[dataSourceCode] = {})) as IDataObject; + + dataSourceStats[fieldName] = value; + if (STATUS_FIELD_CODES.has(fieldCode)) { + dataSourceStats[`${fieldName}Label`] = SESSION_STATUS_LABELS[value as string] || value; + } + } + }); + }); + } + + return normalized; + }); + } + + return []; +} diff --git a/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/index.ts b/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/index.ts new file mode 100644 index 0000000..0df790b --- /dev/null +++ b/nodes/CoveDataProtection/actions/accounts/getBackupStatistics/index.ts @@ -0,0 +1,4 @@ +import { execute } from './execute'; +import { getBackupStatisticsDescription as description } from './description'; + +export { description, execute }; diff --git a/nodes/CoveDataProtection/actions/accounts/getStatistics/description.ts b/nodes/CoveDataProtection/actions/accounts/getStatistics/description.ts index badcc7d..f4cb3e1 100644 --- a/nodes/CoveDataProtection/actions/accounts/getStatistics/description.ts +++ b/nodes/CoveDataProtection/actions/accounts/getStatistics/description.ts @@ -80,8 +80,8 @@ export const getStatisticsDescription: INodeProperties[] = [ value: 'Merged', }, { - name: 'Detailed', - value: 'Detailed', + name: 'Detailed (Per Installation)', + value: 'PerInstallation', }, ], default: 'Merged', diff --git a/nodes/CoveDataProtection/actions/accounts/index.ts b/nodes/CoveDataProtection/actions/accounts/index.ts index b6f6476..901eac7 100644 --- a/nodes/CoveDataProtection/actions/accounts/index.ts +++ b/nodes/CoveDataProtection/actions/accounts/index.ts @@ -1,12 +1,13 @@ import * as getAll from './getAll'; import * as getById from './getById'; import * as getStatistics from './getStatistics'; +import * as getBackupStatistics from './getBackupStatistics'; import * as create from './create'; import * as update from './update'; import * as deleteAccount from './delete'; import { INodeProperties } from 'n8n-workflow'; -export { getAll, getById, getStatistics, create, update, deleteAccount as delete }; +export { getAll, getById, getStatistics, getBackupStatistics, create, update, deleteAccount as delete }; export const description: INodeProperties[] = [ { @@ -32,6 +33,12 @@ export const description: INodeProperties[] = [ description: 'Delete an account', action: 'Delete an account', }, + { + name: 'Get Backup Statistics', + value: 'getBackupStatistics', + description: 'Enumerate backup statistics (most recent backup date and status per data source) for devices', + action: 'Get account backup statistics', + }, { name: 'Get by ID', value: 'getById', @@ -62,6 +69,7 @@ export const description: INodeProperties[] = [ ...getAll.description, ...getById.description, ...getStatistics.description, + ...getBackupStatistics.description, ...create.description, ...update.description, ...deleteAccount.description,