diff --git a/packages/n8n-node/nodes/Probo/GenericFunctions.ts b/packages/n8n-node/nodes/Probo/GenericFunctions.ts index 788e762f8..2da4a4da1 100644 --- a/packages/n8n-node/nodes/Probo/GenericFunctions.ts +++ b/packages/n8n-node/nodes/Probo/GenericFunctions.ts @@ -168,3 +168,78 @@ export async function proboConnectApiRequestAllItems( limit, ); } + +export async function proboApiMultipartRequest( + this: IExecuteFunctions, + query: string, + variables: IDataObject, + fileVariablePath: string, + fileBuffer: Buffer, + fileName: string, + mimeType: string = 'application/octet-stream', +): Promise { + const credentials = await this.getCredentials('proboApi'); + + if (!credentials?.apiKey) { + throw new NodeApiError(this.getNode(), { message: 'API Key is required' } as JsonObject); + } + + const boundary = `----n8nFormBoundary${Date.now().toString(16)}`; + + const safeFileName = fileName + .replace(/[\r\n]/g, '') + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"'); + const safeMimeType = mimeType.replace(/[\r\n]/g, ''); + + const operations = JSON.stringify({ query, variables }); + const map = JSON.stringify({ '0': [fileVariablePath] }); + + const parts: Buffer[] = []; + + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="operations"\r\n\r\n${operations}\r\n`, + )); + + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="map"\r\n\r\n${map}\r\n`, + )); + + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="0"; filename="${safeFileName}"\r\nContent-Type: ${safeMimeType}\r\n\r\n`, + )); + parts.push(fileBuffer); + parts.push(Buffer.from(`\r\n--${boundary}--\r\n`)); + + const body = Buffer.concat(parts); + + const options: IHttpRequestOptions = { + method: 'POST', + baseURL: `${credentials.server}`, + url: '/api/console/v1/graphql', + headers: { + Authorization: `Bearer ${credentials.apiKey}`, + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'User-Agent': `probo-n8n-node/${version}`, + }, + body, + }; + + try { + const response = await this.helpers.httpRequest(options); + + if (response.errors && Array.isArray(response.errors) && response.errors.length > 0) { + const errorMessages = response.errors.map((err: IDataObject) => + err.message || JSON.stringify(err) + ).join('; '); + throw new NodeApiError(this.getNode(), { + message: `GraphQL errors: ${errorMessages}`, + httpCode: '200', + } as JsonObject); + } + + return response; + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} diff --git a/packages/n8n-node/nodes/Probo/Probo.node.ts b/packages/n8n-node/nodes/Probo/Probo.node.ts index 160ae4c05..efd7028ea 100644 --- a/packages/n8n-node/nodes/Probo/Probo.node.ts +++ b/packages/n8n-node/nodes/Probo/Probo.node.ts @@ -67,6 +67,11 @@ export class Probo implements INodeType { value: 'asset', description: 'Manage assets', }, + { + name: 'Audit', + value: 'audit', + description: 'Manage audits', + }, { name: 'Control', value: 'control', diff --git a/packages/n8n-node/nodes/Probo/actions/audit/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/create.operation.ts new file mode 100644 index 000000000..b4e3f8549 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/create.operation.ts @@ -0,0 +1,148 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Framework ID', + name: 'frameworkId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the framework', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['audit'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the audit', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + options: [ + { + name: 'Completed', + value: 'COMPLETED', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Not Started', + value: 'NOT_STARTED', + }, + { + name: 'Outdated', + value: 'OUTDATED', + }, + { + name: 'Rejected', + value: 'REJECTED', + }, + ], + default: 'NOT_STARTED', + description: 'The state of the audit', + }, + { + displayName: 'Valid From', + name: 'validFrom', + type: 'dateTime', + default: '', + description: 'The start date of the audit validity period', + }, + { + displayName: 'Valid Until', + name: 'validUntil', + type: 'dateTime', + default: '', + description: 'The end date of the audit validity period', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const frameworkId = this.getNodeParameter('frameworkId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + state?: string; + validFrom?: string; + validUntil?: string; + }; + + const query = ` + mutation CreateAudit($input: CreateAuditInput!) { + createAudit(input: $input) { + auditEdge { + node { + id + name + state + validFrom + validUntil + reportUrl + trustCenterVisibility + createdAt + updatedAt + } + } + } + } + `; + + const input: Record = { + organizationId, + frameworkId, + }; + if (additionalFields.name) input.name = additionalFields.name; + if (additionalFields.state) input.state = additionalFields.state; + if (additionalFields.validFrom) input.validFrom = additionalFields.validFrom; + if (additionalFields.validUntil) input.validUntil = additionalFields.validUntil; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/audit/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/delete.operation.ts new file mode 100644 index 000000000..1f895e2f0 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/delete.operation.ts @@ -0,0 +1,41 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the audit to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + + const query = ` + mutation DeleteAudit($input: DeleteAuditInput!) { + deleteAudit(input: $input) { + deletedAuditId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { auditId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/audit/deleteReport.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/deleteReport.operation.ts new file mode 100644 index 000000000..2debac9e3 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/deleteReport.operation.ts @@ -0,0 +1,51 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['deleteReport'], + }, + }, + default: '', + description: 'The ID of the audit whose report to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + + const query = ` + mutation DeleteAuditReport($input: DeleteAuditReportInput!) { + deleteAuditReport(input: $input) { + audit { + id + name + state + validFrom + validUntil + reportUrl + trustCenterVisibility + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { auditId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/audit/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/get.operation.ts new file mode 100644 index 000000000..275407ef2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/get.operation.ts @@ -0,0 +1,55 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the audit', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + + const query = ` + query GetAudit($auditId: ID!) { + node(id: $auditId) { + ... on Audit { + id + name + state + validFrom + validUntil + reportUrl + trustCenterVisibility + createdAt + updatedAt + } + } + } + `; + + const variables = { + auditId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/audit/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/getAll.operation.ts new file mode 100644 index 000000000..ef590ae07 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/getAll.operation.ts @@ -0,0 +1,104 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['audit'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['audit'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetAudits($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + audits(first: $first, after: $after) { + edges { + node { + id + name + state + validFrom + validUntil + reportUrl + trustCenterVisibility + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const audits = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.audits as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { audits }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/audit/index.ts b/packages/n8n-node/nodes/Probo/actions/audit/index.ts new file mode 100644 index 000000000..63e774114 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/index.ts @@ -0,0 +1,84 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; +import * as uploadReportOp from './uploadReport.operation'; +import * as deleteReportOp from './deleteReport.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['audit'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new audit', + action: 'Create an audit', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an audit', + action: 'Delete an audit', + }, + { + name: 'Delete Report', + value: 'deleteReport', + description: 'Delete an audit report', + action: 'Delete an audit report', + }, + { + name: 'Get', + value: 'get', + description: 'Get an audit', + action: 'Get an audit', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many audits', + action: 'Get many audits', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing audit', + action: 'Update an audit', + }, + { + name: 'Upload Report', + value: 'uploadReport', + description: 'Upload a report for an audit', + action: 'Upload an audit report', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, + ...uploadReportOp.description, + ...deleteReportOp.description, +]; + +export { + createOp as create, + updateOp as update, + deleteOp as delete, + getOp as get, + getAllOp as getAll, + uploadReportOp as uploadReport, + deleteReportOp as deleteReport, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/audit/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/update.operation.ts new file mode 100644 index 000000000..2f6130326 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/update.operation.ts @@ -0,0 +1,128 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Audit ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the audit to update', + required: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['audit'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the audit', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + options: [ + { + name: 'Completed', + value: 'COMPLETED', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Not Started', + value: 'NOT_STARTED', + }, + { + name: 'Outdated', + value: 'OUTDATED', + }, + { + name: 'Rejected', + value: 'REJECTED', + }, + ], + default: 'NOT_STARTED', + description: 'The state of the audit', + }, + { + displayName: 'Valid From', + name: 'validFrom', + type: 'dateTime', + default: '', + description: 'The start date of the audit validity period', + }, + { + displayName: 'Valid Until', + name: 'validUntil', + type: 'dateTime', + default: '', + description: 'The end date of the audit validity period', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const id = this.getNodeParameter('id', itemIndex) as string; + const updateFields = this.getNodeParameter('updateFields', itemIndex, {}) as { + name?: string; + state?: string; + validFrom?: string; + validUntil?: string; + }; + + const query = ` + mutation UpdateAudit($input: UpdateAuditInput!) { + updateAudit(input: $input) { + audit { + id + name + state + validFrom + validUntil + reportUrl + trustCenterVisibility + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id }; + if (updateFields.name) input.name = updateFields.name; + if (updateFields.state) input.state = updateFields.state; + if (updateFields.validFrom) input.validFrom = updateFields.validFrom; + if (updateFields.validUntil) input.validUntil = updateFields.validUntil; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/audit/uploadReport.operation.ts b/packages/n8n-node/nodes/Probo/actions/audit/uploadReport.operation.ts new file mode 100644 index 000000000..a2fb58395 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/audit/uploadReport.operation.ts @@ -0,0 +1,87 @@ +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiMultipartRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['uploadReport'], + }, + }, + default: '', + description: 'The ID of the audit to upload the report for', + required: true, + }, + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + type: 'string', + displayOptions: { + show: { + resource: ['audit'], + operation: ['uploadReport'], + }, + }, + default: 'data', + description: 'The name of the input field containing the binary file data to upload', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex) as string; + + const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName); + const fileBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); + + const fileName = binaryData.fileName || 'report'; + const mimeType = binaryData.mimeType || 'application/octet-stream'; + + const query = ` + mutation UploadAuditReport($input: UploadAuditReportInput!) { + uploadAuditReport(input: $input) { + audit { + id + name + state + validFrom + validUntil + reportUrl + trustCenterVisibility + createdAt + updatedAt + } + } + } + `; + + const variables = { + input: { + auditId, + file: null, + }, + }; + + const responseData = await proboApiMultipartRequest.call( + this, + query, + variables, + 'variables.input.file', + fileBuffer, + fileName, + mimeType, + ); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/index.ts b/packages/n8n-node/nodes/Probo/actions/index.ts index 50a52df90..00cb27b45 100644 --- a/packages/n8n-node/nodes/Probo/actions/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/index.ts @@ -1,5 +1,6 @@ import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; import * as asset from './asset'; +import * as audit from './audit'; import * as control from './control'; import * as datum from './datum'; import * as execute from './execute'; @@ -23,6 +24,7 @@ export interface OperationModule { export const resources: Record = { asset: asset as ResourceModule, + audit: audit as ResourceModule, control: control as ResourceModule, datum: datum as ResourceModule, execute: execute as ResourceModule,