mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -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';
|
||||
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',
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
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'],
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue