feat(ServiceNow Node): Add attachment functionality (#3137)

*  Add ServiceNow attachment functionality

* 🔨 download fix

*  improvements

*  parameter name fix

*  download attachment for get all operation

*  filters update

*  hint update

*  Small improvements

Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
pemontto 2022-05-27 17:39:55 +01:00 committed by GitHub
parent b687ba11cc
commit c38f6af499
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 464 additions and 2 deletions

View file

@ -13,6 +13,24 @@ export class ServiceNowBasicApi implements ICredentialType {
displayName = 'ServiceNow Basic Auth API';
documentationUrl = 'serviceNow';
properties: INodeProperties[] = [
{
displayName: 'User',
name: 'user',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
required: true,
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Subdomain',
name: 'subdomain',

View file

@ -0,0 +1,290 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const attachmentOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: [
'attachment',
],
},
},
options: [
{
name: 'Upload',
value: 'upload',
description: 'Upload an attachment to a specific table record',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an attachment',
},
{
name: 'Get',
value: 'get',
description: 'Get an attachment',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all attachments on a table',
},
],
default: 'upload',
},
];
export const attachmentFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* attachment common fields */
/* -------------------------------------------------------------------------- */
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTables',
},
default: '',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'upload',
'getAll',
],
},
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* attachment:upload */
/* -------------------------------------------------------------------------- */
{
displayName: 'Table Record ID',
name: 'id',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'upload',
],
},
},
required: true,
description: 'Sys_id of the record in the table specified in Table Name that you want to attach the file to',
},
{
displayName: 'Input Data Field Name',
name: 'inputDataFieldName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'upload',
],
},
},
description: 'Name of the binary property that contains the data to upload',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'upload',
],
},
},
default: {},
options: [
{
displayName: 'File Name for the Attachment',
name: 'file_name',
type: 'string',
default: '',
description: 'Name to give the attachment',
},
],
},
/* -------------------------------------------------------------------------- */
/* attachment:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Attachment ID',
name: 'attachmentId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'delete',
],
},
},
required: true,
description: 'Sys_id value of the attachment to delete',
},
/* -------------------------------------------------------------------------- */
/* attachment:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Attachment ID',
name: 'attachmentId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'get',
],
},
},
required: true,
description: 'Sys_id value of the attachment to delete',
},
/* -------------------------------------------------------------------------- */
/* attachment:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'attachment',
],
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: [
'attachment',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Download Attachments',
name: 'download',
type: 'boolean',
default: false,
required: true,
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'get',
'getAll',
],
},
},
},
{
displayName: 'Output Field',
name: 'outputField',
type: 'string',
default: 'data',
description: 'Field name where downloaded data will be placed',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'get',
'getAll',
],
download: [
true,
],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'attachment',
],
operation: [
'get', 'getAll',
],
},
},
default: {},
options: [
{
displayName: 'Filter',
name: 'queryFilter',
type: 'string',
placeholder: '<col_name><operator><value>',
default: '',
description: 'An encoded query string used to filter the results',
hint: 'All parameters are case-sensitive. Queries can contain more than one entry. <a href="https://developer.servicenow.com/dev.do#!/learn/learning-plans/quebec/servicenow_application_developer/app_store_learnv2_rest_quebec_more_about_query_parameters">more information</a>.',
},
],
},
];

View file

@ -82,9 +82,34 @@ export async function serviceNowRequestAllItems(this: IExecuteFunctions | ILoadO
return returnData;
}
export async function serviceNowDownloadAttachment(
this: IExecuteFunctions,
endpoint: string,
fileName: string,
contentType: string,
) {
const fileData = await serviceNowApiRequest.call(
this,
'GET',
`${endpoint}/file`,
{},
{},
'',
{ json: false, encoding: null, resolveWithFullResponse: true },
);
const binaryData = await this.helpers.prepareBinaryData(
Buffer.from(fileData.body as string),
fileName,
contentType,
);
return binaryData;
}
export const mapEndpoint = (resource: string, operation: string) => {
const resourceEndpoint = new Map([
['attachment', 'sys_dictionary'],
['tableRecord', 'sys_dictionary'],
['businessService', 'cmdb_ci_service'],
['configurationItems', 'cmdb_ci'],

View file

@ -4,6 +4,7 @@ import {
} from 'n8n-core';
import {
IBinaryData,
IDataObject,
INodeExecutionData,
INodePropertyOptions,
@ -16,10 +17,16 @@ import {
import {
mapEndpoint,
serviceNowApiRequest,
serviceNowDownloadAttachment,
serviceNowRequestAllItems,
sortData
} from './GenericFunctions';
import {
attachmentFields,
attachmentOperations,
} from './AttachmentDescription';
import {
businessServiceFields,
businessServiceOperations,
@ -127,6 +134,10 @@ export class ServiceNow implements INodeType {
type: 'options',
noDataExpression: true,
options: [
{
name: 'Attachment',
value: 'attachment',
},
{
name: 'Business Service',
value: 'businessService',
@ -166,7 +177,9 @@ export class ServiceNow implements INodeType {
],
default: 'user',
},
// ATTACHMENT SERVICE
...attachmentOperations,
...attachmentFields,
// BUSINESS SERVICE
...businessServiceOperations,
...businessServiceFields,
@ -456,7 +469,117 @@ export class ServiceNow implements INodeType {
for (let i = 0; i < length; i++) {
try {
if (resource === 'businessService') {
// https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_AttachmentAPI
if (resource === 'attachment') {
if (operation === 'get') {
const attachmentsSysId = this.getNodeParameter('attachmentId', i) as string;
const download = this.getNodeParameter('download', i) as boolean;
const endpoint = `/now/attachment/${attachmentsSysId}`;
const response = await serviceNowApiRequest.call(this, 'GET', endpoint, {});
const fileMetadata = response.result;
responseData = {
json: fileMetadata,
};
if (download) {
const outputField = this.getNodeParameter('outputField', i) as string;
responseData = {
...responseData,
binary: {
[outputField]: await serviceNowDownloadAttachment.call(
this,
endpoint,
fileMetadata.file_name,
fileMetadata.content_type,
),
},
};
}
} else if (operation === 'getAll') {
const download = this.getNodeParameter('download', i) as boolean;
const tableName = this.getNodeParameter('tableName', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
qs = {} as IDataObject;
qs.sysparm_query = `table_name=${tableName}`;
if (options.queryFilter) {
qs.sysparm_query = `${qs.sysparm_query}^${options.queryFilter}`;
}
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
qs.sysparm_limit = limit;
const response = await serviceNowApiRequest.call(this, 'GET', '/now/attachment', {}, qs);
responseData = response.result;
} else {
responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/attachment', {}, qs);
}
if (download) {
const outputField = this.getNodeParameter('outputField', i) as string;
const responseDataWithAttachments: IDataObject[] = [];
for (const data of responseData as IDataObject[]) {
responseDataWithAttachments.push({
json: data,
binary: {
[outputField]: await serviceNowDownloadAttachment.call(
this,
`/now/attachment/${data.sys_id}`,
data.file_name as string,
data.content_type as string,
),
},
});
}
responseData = responseDataWithAttachments;
} else {
responseData = (responseData as IDataObject[]).map( data => ({ json: data }));
}
} else if (operation === 'upload') {
const tableName = this.getNodeParameter('tableName', i) as string;
const recordId = this.getNodeParameter('id', i) as string;
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
let binaryData: IBinaryData;
if (items[i].binary && items[i].binary![inputDataFieldName]) {
binaryData = items[i].binary![inputDataFieldName];
} else {
throw new NodeOperationError(this.getNode(), `No binary data property "${inputDataFieldName}" does not exists on item!`);
}
const headers: IDataObject = {
'Content-Type': binaryData.mimeType,
};
const qs: IDataObject = {
table_name: tableName,
table_sys_id: recordId,
file_name: binaryData.fileName ? binaryData.fileName : `${inputDataFieldName}.${binaryData.fileExtension}`,
...options,
};
const body = await this.helpers.getBinaryDataBuffer(i, inputDataFieldName) as Buffer;
const response = await serviceNowApiRequest.call(this, 'POST', '/now/attachment/file', body, qs, '', {headers});
responseData = response.result;
} else if (operation === 'delete') {
const attachmentsSysId = this.getNodeParameter('attachmentId', i) as string;
await serviceNowApiRequest.call(this, 'DELETE', `/now/attachment/${attachmentsSysId}`);
responseData = {'success': true};
}
} else if (resource === 'businessService') {
if (operation === 'getAll') {
@ -818,6 +941,12 @@ export class ServiceNow implements INodeType {
? returnData.push(...responseData)
: returnData.push(responseData);
}
if (resource === 'attachment') {
if (operation === 'get' || operation === 'getAll') {
return this.prepareOutputData(returnData as INodeExecutionData[]);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}