mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
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:
parent
b687ba11cc
commit
c38f6af499
|
@ -13,6 +13,24 @@ export class ServiceNowBasicApi implements ICredentialType {
|
||||||
displayName = 'ServiceNow Basic Auth API';
|
displayName = 'ServiceNow Basic Auth API';
|
||||||
documentationUrl = 'serviceNow';
|
documentationUrl = 'serviceNow';
|
||||||
properties: INodeProperties[] = [
|
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',
|
displayName: 'Subdomain',
|
||||||
name: 'subdomain',
|
name: 'subdomain',
|
||||||
|
|
290
packages/nodes-base/nodes/ServiceNow/AttachmentDescription.ts
Normal file
290
packages/nodes-base/nodes/ServiceNow/AttachmentDescription.ts
Normal 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>.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
|
@ -82,9 +82,34 @@ export async function serviceNowRequestAllItems(this: IExecuteFunctions | ILoadO
|
||||||
return returnData;
|
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) => {
|
export const mapEndpoint = (resource: string, operation: string) => {
|
||||||
const resourceEndpoint = new Map([
|
const resourceEndpoint = new Map([
|
||||||
|
['attachment', 'sys_dictionary'],
|
||||||
['tableRecord', 'sys_dictionary'],
|
['tableRecord', 'sys_dictionary'],
|
||||||
['businessService', 'cmdb_ci_service'],
|
['businessService', 'cmdb_ci_service'],
|
||||||
['configurationItems', 'cmdb_ci'],
|
['configurationItems', 'cmdb_ci'],
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
IBinaryData,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodePropertyOptions,
|
INodePropertyOptions,
|
||||||
|
@ -16,10 +17,16 @@ import {
|
||||||
import {
|
import {
|
||||||
mapEndpoint,
|
mapEndpoint,
|
||||||
serviceNowApiRequest,
|
serviceNowApiRequest,
|
||||||
|
serviceNowDownloadAttachment,
|
||||||
serviceNowRequestAllItems,
|
serviceNowRequestAllItems,
|
||||||
sortData
|
sortData
|
||||||
} from './GenericFunctions';
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
attachmentFields,
|
||||||
|
attachmentOperations,
|
||||||
|
} from './AttachmentDescription';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
businessServiceFields,
|
businessServiceFields,
|
||||||
businessServiceOperations,
|
businessServiceOperations,
|
||||||
|
@ -127,6 +134,10 @@ export class ServiceNow implements INodeType {
|
||||||
type: 'options',
|
type: 'options',
|
||||||
noDataExpression: true,
|
noDataExpression: true,
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Attachment',
|
||||||
|
value: 'attachment',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Business Service',
|
name: 'Business Service',
|
||||||
value: 'businessService',
|
value: 'businessService',
|
||||||
|
@ -166,7 +177,9 @@ export class ServiceNow implements INodeType {
|
||||||
],
|
],
|
||||||
default: 'user',
|
default: 'user',
|
||||||
},
|
},
|
||||||
|
// ATTACHMENT SERVICE
|
||||||
|
...attachmentOperations,
|
||||||
|
...attachmentFields,
|
||||||
// BUSINESS SERVICE
|
// BUSINESS SERVICE
|
||||||
...businessServiceOperations,
|
...businessServiceOperations,
|
||||||
...businessServiceFields,
|
...businessServiceFields,
|
||||||
|
@ -456,7 +469,117 @@ export class ServiceNow implements INodeType {
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
try {
|
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') {
|
if (operation === 'getAll') {
|
||||||
|
|
||||||
|
@ -818,6 +941,12 @@ export class ServiceNow implements INodeType {
|
||||||
? returnData.push(...responseData)
|
? returnData.push(...responseData)
|
||||||
: 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)];
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue