diff --git a/packages/nodes-base/credentials/ServiceNowOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ServiceNowOAuth2Api.credentials.ts new file mode 100644 index 0000000000..d12b0cd8f1 --- /dev/null +++ b/packages/nodes-base/credentials/ServiceNowOAuth2Api.credentials.ts @@ -0,0 +1,62 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ServiceNowOAuth2Api implements ICredentialType { + name = 'serviceNowOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'ServiceNow OAuth2 API'; + documentationUrl = 'serviceNow'; + properties = [ + { + displayName: 'Subdomain', + name: 'subdomain', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'n8n', + description: 'The subdomain of your ServiceNow environment', + required: true, + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: '=https://{{$self["subdomain"]}}.service-now.com/oauth_auth.do', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: '=https://{{$self["subdomain"]}}.service-now.com/oauth_token.do', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'useraccount', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'response_type=code', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'grant_type=authorization_code', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/nodes/ServiceNow/BusinessServiceDescription.ts b/packages/nodes-base/nodes/ServiceNow/BusinessServiceDescription.ts new file mode 100644 index 0000000000..650480e9f2 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/BusinessServiceDescription.ts @@ -0,0 +1,137 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const businessServiceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'businessService', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + }, +] as INodeProperties[]; + +export const businessServiceFields = [ + + /* -------------------------------------------------------------------------- */ + /* businessService:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'businessService', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'businessService', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'businessService', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/ConfigurationItemsDescription.ts b/packages/nodes-base/nodes/ServiceNow/ConfigurationItemsDescription.ts new file mode 100644 index 0000000000..bb95b3a955 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/ConfigurationItemsDescription.ts @@ -0,0 +1,137 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const configurationItemsOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'configurationItems', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + }, +] as INodeProperties[]; + +export const configurationItemsFields = [ + + /* -------------------------------------------------------------------------- */ + /* configurationItems:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'configurationItems', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'configurationItems', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'configurationItems', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/DepartmentDescription.ts b/packages/nodes-base/nodes/ServiceNow/DepartmentDescription.ts new file mode 100644 index 0000000000..cc4a3871b4 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/DepartmentDescription.ts @@ -0,0 +1,137 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const departmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'department', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + }, +] as INodeProperties[]; + +export const departmentFields = [ + + /* -------------------------------------------------------------------------- */ + /* department:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'department', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'department', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'department', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/DictionaryDescription.ts b/packages/nodes-base/nodes/ServiceNow/DictionaryDescription.ts new file mode 100644 index 0000000000..fd0f14cd4f --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/DictionaryDescription.ts @@ -0,0 +1,137 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const dictionaryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'dictionary', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + }, +] as INodeProperties[]; + +export const dictionaryFields = [ + + /* -------------------------------------------------------------------------- */ + /* dictionary:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'dictionary', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'dictionary', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'dictionary', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/GenericFunctions.ts b/packages/nodes-base/nodes/ServiceNow/GenericFunctions.ts new file mode 100644 index 0000000000..4b069a4929 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/GenericFunctions.ts @@ -0,0 +1,98 @@ +import { + OptionsWithUri +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodePropertyOptions, + NodeApiError, +} from 'n8n-workflow'; + +export async function serviceNowApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('serviceNowOAuth2Api'); + + const options: OptionsWithUri = { + headers: {}, + method, + qs, + body, + uri: uri || `https://${credentials?.subdomain}.service-now.com/api${resource}`, + json: true, + }; + if (!Object.keys(body).length) { + delete options.body; + } + + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + if (options.qs.limit) { + delete options.qs.limit; + } + + try { + + return await this.helpers.requestOAuth2!.call(this, 'serviceNowOAuth2Api', options); + + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function serviceNowRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + let responseData; + + const page = 100; + + query.sysparm_limit = page; + + responseData = await serviceNowApiRequest.call(this, method, resource, body, query, undefined, { resolveWithFullResponse: true }); + returnData.push.apply(returnData, responseData.body.result); + + const quantity = responseData.headers['x-total-count']; + const iterations = Math.round(quantity / page) + (quantity % page ? 1 : 0); + + for (let iteration = 1; iteration < iterations; iteration++) { + query.sysparm_limit = page; + query.sysparm_offset = iteration * page; + responseData = await serviceNowApiRequest.call(this, method, resource, body, query, undefined, { resolveWithFullResponse: true }); + + returnData.push.apply(returnData, responseData.body.result); + } + + return returnData; +} + + +export const mapEndpoint = (resource: string, operation: string) => { + const resourceEndpoint = new Map([ + ['tableRecord', 'sys_dictionary'], + ['businessService', 'cmdb_ci_service'], + ['configurationItems', 'cmdb_ci'], + ['department', 'cmn_department'], + ['dictionary', 'sys_dictionary'], + ['incident', 'incident'], + ['user', 'sys_user'], + ['userGroup', 'sys_user_group'], + ['userRole', 'sys_user_role'], + ]); + return resourceEndpoint.get(resource); +}; + +export const sortData = (returnData: INodePropertyOptions[]): INodePropertyOptions[] => { + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; +}; diff --git a/packages/nodes-base/nodes/ServiceNow/IncidentDescription.ts b/packages/nodes-base/nodes/ServiceNow/IncidentDescription.ts new file mode 100644 index 0000000000..41d1537ab5 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/IncidentDescription.ts @@ -0,0 +1,673 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const incidentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'incident', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'get', + }, +] as INodeProperties[]; + +export const incidentFields = [ + /* -------------------------------------------------------------------------- */ + /* incident:create */ + /* -------------------------------------------------------------------------- */ + + { + displayName: 'Short Description', + name: 'short_description', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'Short description of the incident', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Assigned To', + name: 'assigned_to', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'additionalFields.assignment_group', + ], + }, + default: '', + description: 'Which user is the incident assigned to. Requires the selection of an assignment group', + }, + { + displayName: 'Assignment Group', + name: 'assignment_group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAssignmentGroups', + }, + default: '', + description: 'The assignment group of the incident', + }, + { + displayName: 'Business Service', + name: 'business_service', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBusinessServices', + }, + default: '', + description: 'The business service', + }, + { + displayName: 'Caller ID', + name: 'caller_id', + type: 'string', + default: '', + description: 'The unique identifier of the caller of the incident', + }, + { + displayName: 'Category', + name: 'category', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentCategories', + }, + default: '', + description: 'The category of the incident', + }, + { + displayName: 'Close Notes', + name: 'close_notes', + type: 'string', + default: '', + description: 'The close notes for the incident', + }, + { + displayName: 'Configuration Items', + name: 'cmdb_ci', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getConfigurationItems', + }, + default: '', + description: 'Configuration Items, \'cmdb_ci\' in metadata', + }, + { + displayName: 'Contact Type', + name: 'contact_type', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Phone', + value: 'phone', + }, + { + name: 'Self Service', + value: 'self-service', + }, + { + name: 'Walk In', + value: 'walk-in', + }, + ], + default: '', + description: 'The contact type', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the incident', + }, + { + displayName: 'Impact', + name: 'impact', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + default: '', + description: 'The impact of the incident', + }, + { + displayName: 'Resolution Code', + name: 'close_code', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentResolutionCodes', + }, + default: '', + description: 'The resolution code of the incident. \'close_code\' in metadata', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentStates', + }, + default: '', + description: 'The state of the incident', + }, + { + displayName: 'Subcategory', + name: 'subcategory', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentSubcategories', + loadOptionsDependsOn: [ + 'additionalFields.category', + ], + }, + default: '', + description: 'The subcategory of the incident', + }, + { + displayName: 'Urgency', + name: 'urgency', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + default: '', + description: 'The urgency of the incident', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* incident:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'incident', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'incident', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* incident:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Incident ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'delete', + 'get', + ], + }, + }, + required: true, + description: 'Unique identifier of the incident', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'get', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* incident:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Incident ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + description: 'Unique identifier of the incident', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'incident', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Assigned To', + name: 'assigned_to', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'additionalFields.assignment_group', + ], + }, + default: '', + description: 'Which user is the incident assigned to. Requires the selection of an assignment group', + }, + { + displayName: 'Assignment Group', + name: 'assignment_group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAssignmentGroups', + }, + default: '', + description: 'The assignment group of the incident', + }, + { + displayName: 'Business Service', + name: 'business_service', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBusinessServices', + }, + default: '', + description: 'The business service', + }, + { + displayName: 'Caller ID', + name: 'caller_id', + type: 'string', + default: '', + description: 'The unique identifier of the caller of the incident', + }, + { + displayName: 'Category', + name: 'category', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentCategories', + }, + default: '', + description: 'The category of the incident', + }, + { + displayName: 'Close Notes', + name: 'close_notes', + type: 'string', + default: '', + description: 'The close notes for the incident', + }, + { + displayName: 'Configuration Items', + name: 'cmdb_ci', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getConfigurationItems', + }, + default: '', + description: 'Configuration Items, \'cmdb_ci\' in metadata', + }, + { + displayName: 'Contact Type', + name: 'contact_type', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Phone', + value: 'phone', + }, + { + name: 'Self Service', + value: 'self-service', + }, + { + name: 'Walk In', + value: 'walk-in', + }, + ], + default: '', + description: 'The contact type', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the incident', + }, + { + displayName: 'Impact', + name: 'impact', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + default: '', + description: 'The impact of the incident', + }, + { + displayName: 'Resolution Code', + name: 'close_code', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentResolutionCodes', + }, + default: '', + description: 'The resolution code of the incident. \'close_code\' in metadata', + }, + { + displayName: 'On Hold Reason', + name: 'hold_reason', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentHoldReasons', + }, + default: '', + description: 'The on hold reason for the incident. It applies if the state is On Hold', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentStates', + }, + default: '', + description: 'The state of the incident', + }, + { + displayName: 'Subcategory', + name: 'subcategory', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIncidentSubcategories', + loadOptionsDependsOn: [ + 'additionalFields.category', + ], + }, + default: '', + description: 'The subcategory of the incident', + }, + { + displayName: 'Urgency', + name: 'urgency', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + default: '', + description: 'The urgency of the incident', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts b/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts new file mode 100644 index 0000000000..05eda728da --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts @@ -0,0 +1,788 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + mapEndpoint, + serviceNowApiRequest, + serviceNowRequestAllItems, + sortData +} from './GenericFunctions'; + +import { + businessServiceFields, + businessServiceOperations, +} from './BusinessServiceDescription'; + +import { + configurationItemsFields, + configurationItemsOperations, +} from './ConfigurationItemsDescription'; + +import { + departmentFields, + departmentOperations, +} from './DepartmentDescription'; + +import { + dictionaryFields, + dictionaryOperations, +} from './DictionaryDescription'; + +import { + incidentFields, + incidentOperations, +} from './IncidentDescription'; + +import { + tableRecordFields, + tableRecordOperations, +} from './TableRecordDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + +import { + userGroupFields, + userGroupOperations, +} from './UserGroupDescription'; + +import { + userRoleFields, + userRoleOperations, +} from './UserRoleDescription'; + +export class ServiceNow implements INodeType { + description: INodeTypeDescription = { + displayName: 'ServiceNow', + name: 'serviceNow', + icon: 'file:servicenow.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume ServiceNow API', + defaults: { + name: 'ServiceNow', + color: '#81b5a1', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'serviceNowOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Business Service', + value: 'businessService', + }, + { + name: 'Configuration Items', + value: 'configurationItems', + }, + { + name: 'Department', + value: 'department', + }, + { + name: 'Dictionary', + value: 'dictionary', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Table Record', + value: 'tableRecord', + }, + { + name: 'User', + value: 'user', + }, + { + name: 'User Group', + value: 'userGroup', + }, + { + name: 'User Role', + value: 'userRole', + }, + ], + default: 'user', + description: 'Resource to consume', + }, + + // BUSINESS SERVICE + ...businessServiceOperations, + ...businessServiceFields, + // CONFIGURATION ITEMS + ...configurationItemsOperations, + ...configurationItemsFields, + // DEPARTMENT + ...departmentOperations, + ...departmentFields, + // DICTIONARY + ...dictionaryOperations, + ...dictionaryFields, + // INCIDENT + ...incidentOperations, + ...incidentFields, + // TABLE RECORD + ...tableRecordOperations, + ...tableRecordFields, + // USER + ...userOperations, + ...userFields, + // USER GROUP + ...userGroupOperations, + ...userGroupFields, + // USER ROLE + ...userRoleOperations, + ...userRoleFields, + ], + }; + + methods = { + loadOptions: { + async getTables(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const response = await serviceNowApiRequest.call(this, 'GET', `/now/doc/table/schema`, {}, {}); + for (const table of response.result) { + returnData.push({ + name: table.label, + value: table.value, + description: table.value, + }); + } + return sortData(returnData); + }, + // Get all the table column to display them to user + async getColumns(this: ILoadOptionsFunctions): Promise { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const returnData: INodePropertyOptions[] = []; + let tableName; + if (resource === 'tableRecord') { + tableName = this.getNodeParameter('tableName') as string; + } else { + tableName = mapEndpoint(resource, operation); + } + + const qs = { + sysparm_query: `name=${tableName}`, + sysparm_fields: 'column_label,element', + }; + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/sys_dictionary`, {}, qs); + for (const column of response.result) { + if (column.element) { + returnData.push({ + name: column.column_label, + value: column.element, + }); + } + } + return sortData(returnData); + }, + async getBusinessServices(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'name,sys_id', + }; + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/cmdb_ci_service`, {}, qs); + + for (const column of response.result) { + returnData.push({ + name: column.name, + value: column.sys_id, + }); + } + return sortData(returnData); + }, + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const qs = { + sysparm_fields: 'sys_id,user_name', + }; + if (resource === 'incident' && operation === 'create') { + const additionalFields = this.getNodeParameter('additionalFields') as IDataObject; + const group = additionalFields.assignment_group; + + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user_grmember', {}, { + sysparm_query: `group=${group}^`, + }); + + for (const column of response) { + if (column.user) { + + const responseData = await serviceNowApiRequest.call(this, 'GET', `/now/table/sys_user/${column.user.value}`, {}, {}); + const user = responseData.result; + + returnData.push({ + name: user.user_name, + value: user.sys_id, + }); + } + } + } else { + + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user', {}, qs); + + for (const column of response) { + if (column.user_name) { + returnData.push({ + name: column.user_name, + value: column.sys_id, + }); + } + } + } + return sortData(returnData); + }, + async getAssignmentGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'sys_id,name', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user_group', {}, qs); + + for (const column of response) { + if (column.name) { + returnData.push({ + name: column.name, + value: column.sys_id, + }); + } + } + return sortData(returnData); + }, + async getUserRoles(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'sys_id,name', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user_role', {}, qs); + + for (const column of response) { + if (column.name) { + returnData.push({ + name: column.name, + value: column.sys_id, + }); + } + } + return sortData(returnData); + }, + async getConfigurationItems(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'sys_id,name,sys_class_name', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/cmdb_ci', {}, qs); + + for (const column of response) { + if (column.name) { + returnData.push({ + name: column.name, + value: column.sys_id, + description: column.sys_class_name, + }); + } + } + return sortData(returnData); + }, + async getIncidentCategories(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'label,value', + sysparm_query: 'element=category^name=incident', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_choice', {}, qs); + + for (const column of response) { + returnData.push({ + name: column.label, + value: column.value, + }); + + } + return sortData(returnData); + }, + async getIncidentSubcategories(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const operation = this.getNodeParameter('operation'); + let category; + if (operation === 'update') { + const updateFields = this.getNodeParameter('updateFields') as IDataObject; + category = updateFields.category; + } else { + const additionalFields = this.getNodeParameter('additionalFields') as IDataObject; + category = additionalFields.category; + } + const qs = { + sysparm_fields: 'label,value', + sysparm_query: `name=incident^element=subcategory^dependent_value=${category}`, + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_choice', {}, qs); + + for (const column of response) { + returnData.push({ + name: column.label, + value: column.value, + }); + } + + return sortData(returnData); + }, + async getIncidentStates(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'label,value', + sysparm_query: 'element=state^name=incident', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_choice', {}, qs); + + for (const column of response) { + returnData.push({ + name: column.label, + value: column.value, + }); + + } + return sortData(returnData); + }, + async getIncidentResolutionCodes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'label,value', + sysparm_query: 'element=close_code^name=incident', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_choice', {}, qs); + + for (const column of response) { + returnData.push({ + name: column.label, + value: column.value, + }); + + } + return sortData(returnData); + }, + async getIncidentHoldReasons(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + sysparm_fields: 'label,value', + sysparm_query: 'element=hold_reason^name=incident', + }; + const response = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_choice', {}, qs); + + for (const column of response) { + returnData.push({ + name: column.label, + value: column.value, + }); + + } + return sortData(returnData); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length; + let responseData = {}; + let qs: IDataObject; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + try { + if (resource === 'businessService') { + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/cmdb_ci_service', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/cmdb_ci_service', {}, qs); + } + + } + } else if (resource === 'configurationItems') { + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/cmdb_ci', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/cmdb_ci', {}, qs); + } + + } + } else if (resource === 'department') { + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/cmn_department', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/cmn_department', {}, qs); + } + + } + } else if (resource === 'dictionary') { + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/sys_dictionary', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_dictionary', {}, qs); + } + + } + } else if (resource === 'incident') { + + if (operation === 'create') { + + const shortDescription = this.getNodeParameter('short_description', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body = { + short_description: shortDescription, + ...additionalFields, + }; + + const response = await serviceNowApiRequest.call(this, 'POST', `/now/table/incident`, body); + responseData = response.result; + + } else if (operation === 'delete') { + + const id = this.getNodeParameter('id', i) as string; + responseData = await serviceNowApiRequest.call(this, 'DELETE', `/now/table/incident/${id}`); + responseData = { success: true }; + + } else if (operation === 'get') { + + const id = this.getNodeParameter('id', i) as string; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/incident/${id}`, {}, qs); + responseData = response.result; + + } else if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/incident`, {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', `/now/table/incident`, {}, qs); + } + + } else if (operation === 'update') { + + const id = this.getNodeParameter('id', i) as string; + const body = this.getNodeParameter('updateFields', i) as IDataObject; + + const response = await serviceNowApiRequest.call(this, 'PATCH', `/now/table/incident/${id}`, body); + responseData = response.result; + + } else { + throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); + } + } else if (resource === 'tableRecord') { + + if (operation === 'create') { + + const tableName = this.getNodeParameter('tableName', i) as string; + const dataToSend = this.getNodeParameter('dataToSend', i) as string; + let body = {}; + + if (dataToSend === 'mapInput') { + const inputsToIgnore = (this.getNodeParameter('inputsToIgnore', i) as string).split(',').map(field => field.trim()); + body = Object.entries(items[i].json) + .filter(([key]) => !inputsToIgnore.includes(key)) + .reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {}); + } else if (dataToSend === 'columns') { + const fieldsToSend = this.getNodeParameter('fieldsToSend', i) as { + field: IDataObject[] + }; + body = fieldsToSend.field.reduce((obj, field) => { + obj[field.column as string] = field.value; + return obj; + }, {}); + } + + const response = await serviceNowApiRequest.call(this, 'POST', `/now/table/${tableName}`, body); + responseData = response.result; + + } else if (operation === 'delete') { + + const tableName = this.getNodeParameter('tableName', i) as string; + const id = this.getNodeParameter('id', i) as string; + responseData = await serviceNowApiRequest.call(this, 'DELETE', `/now/table/${tableName}/${id}`); + responseData = { success: true }; + + } else if (operation === 'get') { + + const tableName = this.getNodeParameter('tableName', i) as string; + const id = this.getNodeParameter('id', i) as string; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/${tableName}/${id}`, {}, qs); + responseData = response.result; + + } else if (operation === 'getAll') { + + const tableName = this.getNodeParameter('tableName', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/${tableName}`, {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', `/now/table/${tableName}`, {}, qs); + } + + + } else if (operation === 'update') { + + const tableName = this.getNodeParameter('tableName', i) as string; + const id = this.getNodeParameter('id', i) as string; + const dataToSend = this.getNodeParameter('dataToSend', i) as string; + let body = {}; + + if (dataToSend === 'mapInput') { + const inputsToIgnore = (this.getNodeParameter('inputsToIgnore', i) as string).split(',').map(field => field.trim()); + body = Object.entries(items[i].json) + .filter(([key]) => !inputsToIgnore.includes(key)) + .reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {}); + } else if (dataToSend === 'columns') { + const fieldsToSend = this.getNodeParameter('fieldsToSend', i) as { + field: IDataObject[] + }; + body = fieldsToSend.field.reduce((obj, field) => { + obj[field.column as string] = field.value; + return obj; + }, {}); + } + + const response = await serviceNowApiRequest.call(this, 'PATCH', `/now/table/${tableName}/${id}`, body); + responseData = response.result; + + } else { + throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); + } + } else if (resource === 'user') { + + if (operation === 'create') { + + const body = this.getNodeParameter('additionalFields', i) as IDataObject; + + const response = await serviceNowApiRequest.call(this, 'POST', '/now/table/sys_user', body); + responseData = response.result; + + } else if (operation === 'delete') { + + const id = this.getNodeParameter('id', i) as string; + responseData = await serviceNowApiRequest.call(this, 'DELETE', `/now/table/sys_user/${id}`); + responseData = { success: true }; + + } else if (operation === 'get') { + + const getOption = this.getNodeParameter('getOption', i) as string; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (getOption === 'id') { + const id = this.getNodeParameter('id', i) as string; + const response = await serviceNowApiRequest.call(this, 'GET', `/now/table/sys_user/${id}`, {}, qs); + responseData = response.result; + } else { + const userName = this.getNodeParameter('user_name', i) as string; + qs.sysparm_query = `user_name=${userName}`; + qs.sysparm_limit = 1; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/sys_user', {}, qs); + responseData = response.result; + } + + } else if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/sys_user', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user', {}, qs); + } + + } else if (operation === 'update') { + + const id = this.getNodeParameter('id', i) as string; + const body = this.getNodeParameter('updateFields', i) as IDataObject; + + const response = await serviceNowApiRequest.call(this, 'PATCH', `/now/table/sys_user/${id}`, body); + responseData = response.result; + + } else { + throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); + } + } else if (resource === 'userGroup') { + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/sys_user_group', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user_group', {}, qs); + } + } else { + throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); + } + } else if (resource === 'userRole') { + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs = this.getNodeParameter('options', i) as IDataObject; + + if (qs.sysparm_fields) { + qs.sysparm_fields = (qs.sysparm_fields as string[]).join(','); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.sysparm_limit = limit; + const response = await serviceNowApiRequest.call(this, 'GET', '/now/table/sys_user_role', {}, qs); + responseData = response.result; + } else { + responseData = await serviceNowRequestAllItems.call(this, 'GET', '/now/table/sys_user_role', {}, qs); + } + } else { + throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); + } + } else { + throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known!`); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ServiceNow/TableRecordDescription.ts b/packages/nodes-base/nodes/ServiceNow/TableRecordDescription.ts new file mode 100644 index 0000000000..51c9845a44 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/TableRecordDescription.ts @@ -0,0 +1,555 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tableRecordOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'get', + }, +] as INodeProperties[]; + +export const tableRecordFields = [ + /* -------------------------------------------------------------------------- */ + /* tableRecord:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'The table name', + }, + { + displayName: 'Data to Send', + name: 'dataToSend', + type: 'options', + options: [ + { + name: 'Auto-map Input Data to Columns', + value: 'mapInput', + description: 'Use when node input names match destination field names', + }, + { + name: 'Define Below for Each Column', + value: 'columns', + description: 'Set the value for each destination column', + }, + { + name: 'Nothing', + value: 'nothing', + description: `Don't send any column data`, + }, + ], + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'create', + ], + }, + }, + default: 'columns', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'create', + ], + dataToSend: [ + 'mapInput', + ], + }, + }, + default: '', + required: false, + description: 'List of input properties to avoid sending, separated by commas. Leave empty to send all inputs', + }, + { + displayName: 'Fields to Send', + name: 'fieldsToSend', + type: 'fixedCollection', + placeholder: 'Add field to send', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'create', + ], + dataToSend: [ + 'columns', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'field', + values: [ + { + displayName: 'Field Name', + name: 'column', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'tableName', + ], + }, + default: '', + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* tableRecord:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'getAll', + ], + }, + }, + required: true, + description: 'The table name', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tableRecord', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tableRecord', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'tableName', + ], + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* tableRecord:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'delete', + 'get', + ], + }, + }, + required: true, + description: 'Name of the table in which the record exists', + }, + { + displayName: 'Table Record ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'delete', + 'get', + ], + }, + }, + required: true, + description: 'Unique identifier of the record', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'get', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'tableName', + ], + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* tableRecord:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + description: 'The table name', + }, + { + displayName: 'Table Record ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + description: 'Unique identifier of the record', + }, + { + displayName: 'Data to Send', + name: 'dataToSend', + type: 'options', + options: [ + { + name: 'Auto-map Input Data to Columns', + value: 'mapInput', + description: 'Use when node input names match destination field names', + }, + { + name: 'Define Below for Each Column', + value: 'columns', + description: 'Set the value for each destination column', + }, + { + name: 'Nothing', + value: 'nothing', + description: `Don't send any column data`, + }, + ], + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'update', + ], + }, + }, + default: 'columns', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'update', + ], + dataToSend: [ + 'mapInput', + ], + }, + }, + default: '', + required: false, + description: 'List of input properties to avoid sending, separated by commas. Leave empty to send all inputs', + }, + { + displayName: 'Fields to Send', + name: 'fieldsToSend', + type: 'fixedCollection', + placeholder: 'Add field to send', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'tableRecord', + ], + operation: [ + 'update', + ], + dataToSend: [ + 'columns', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'field', + values: [ + { + displayName: 'Field Name', + name: 'column', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'tableName', + ], + }, + default: '', + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/UserDescription.ts b/packages/nodes-base/nodes/ServiceNow/UserDescription.ts new file mode 100644 index 0000000000..645d7efa75 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/UserDescription.ts @@ -0,0 +1,732 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'get', + }, +] as INodeProperties[]; + +export const userFields = [ + /* -------------------------------------------------------------------------- */ + /* user:create */ + /* -------------------------------------------------------------------------- */ + + { + displayName: 'Short Description', + name: 'short_description', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'Short description of the user', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: '', + description: 'Whether to activate the user', + }, + { + displayName: 'Building', + name: 'building', + type: 'string', + default: '', + description: 'The Building address', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City of the user', + }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'The name of the company for the user', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country of the user', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + description: 'Department of the user', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The email address associated with the user', + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + description: 'The first name of the user', + }, + { + displayName: 'Gender', + name: 'gender', + type: 'string', + default: '', + description: 'The gender of the user', + }, + { + displayName: 'Home Phone', + name: 'home_phone', + type: 'string', + default: '', + description: 'Home phone of the user', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'The last name of the user', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the user', + }, + { + displayName: 'Manager', + name: 'manager', + type: 'string', + default: '', + description: 'Manager of the user', + }, + { + displayName: 'Middle Name', + name: 'middle_name', + type: 'string', + default: '', + description: 'The middle name of the user', + }, + { + displayName: 'Mobile Phone', + name: 'mobile_phone', + type: 'string', + default: '', + description: 'Mobile phone number of the user', + }, + { + displayName: 'Password', + name: 'user_password', + type: 'string', + default: '', + description: 'The user\'s password', + }, + { + displayName: 'Password Needs Reset', + name: 'password_needs_reset', + type: 'boolean', + default: '', + description: 'Whether to require a password reset when the user logs in', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'The main phone number of the user', + }, + { + displayName: 'Roles', + name: 'roles', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUserRoles', + }, + default: '', + description: 'Roles of the user', + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + description: 'The source', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State for the user', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + description: 'Street information for the user separated by comma', + }, + { + displayName: 'Username', + name: 'user_name', + type: 'string', + default: '', + description: 'A username associated with the user (e.g. user_name.123)', + }, + { + displayName: 'Zip Code', + name: 'zip', + type: 'string', + default: '', + description: 'Zip code for the user', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'user', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'user', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'The max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'operation', + ], + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Filter', + name: 'sysparm_query', + type: 'string', + default: '', + description: 'An encoded query string used to filter the results. More info', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* user:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Retrieve Identifier', + name: 'getOption', + type: 'options', + default: 'id', + options: [ + { + name: 'ID', + value: 'id', + }, + { + name: 'Username', + value: 'user_name', + }, + ], + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + required: true, + description: 'Unique identifier of the user', + }, + { + displayName: 'Username', + name: 'user_name', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + getOption: [ + 'user_name', + ], + }, + }, + required: true, + description: 'Unique identifier of the user', + }, + { + displayName: 'User ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + getOption: [ + 'id', + ], + }, + }, + required: true, + description: 'Unique identifier of the user', + }, + { + displayName: 'User ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'delete', + ], + }, + }, + required: true, + description: 'Unique identifier of the user', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Exclude Reference Link', + name: 'sysparm_exclude_reference_link', + type: 'boolean', + default: false, + description: 'Whether to exclude Table API links for reference fields', + }, + { + displayName: 'Fields', + name: 'sysparm_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'operation', + ], + }, + default: '', + description: 'A list of fields to return', + }, + { + displayName: 'Return Values', + name: 'sysparm_display_value', + type: 'options', + options: [ + { + name: 'Actual Values', + value: 'false', + }, + { + name: 'Both', + value: 'all', + }, + { + name: 'Display Values', + value: 'true', + }, + ], + default: 'false', + description: 'Choose which values to return', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* user:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + description: 'Unique identifier of the user', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: '', + description: 'Whether to activate the user', + }, + { + displayName: 'Building', + name: 'building', + type: 'string', + default: '', + description: 'The Building address', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City of the user', + }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'The name of the company for the user', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country of the user', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + description: 'Department of the user', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The email address associated with the user', + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + description: 'The first name of the user', + }, + { + displayName: 'Gender', + name: 'gender', + type: 'string', + default: '', + description: 'The gender of the user', + }, + { + displayName: 'Home Phone', + name: 'home_phone', + type: 'string', + default: '', + description: 'Home phone of the user', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'The last name of the user', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the user', + }, + { + displayName: 'Manager', + name: 'manager', + type: 'string', + default: '', + description: 'Manager of the user', + }, + { + displayName: 'Middle Name', + name: 'middle_name', + type: 'string', + default: '', + description: 'The middle name of the user', + }, + { + displayName: 'Mobile Phone', + name: 'mobile_phone', + type: 'string', + default: '', + description: 'Mobile phone number of the user', + }, + { + displayName: 'Password', + name: 'user_password', + type: 'string', + default: '', + description: 'The user\'s password', + }, + { + displayName: 'Password Needs Reset', + name: 'password_needs_reset', + type: 'boolean', + default: '', + description: 'Whether to require a password reset when the user logs in', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'The main phone number of the user', + }, + { + displayName: 'Roles', + name: 'roles', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUserRoles', + }, + default: '', + description: 'Roles of the user', + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + description: 'The source', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State for the user', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + description: 'Street information for the user separated by comma', + }, + { + displayName: 'Username', + name: 'user_name', + type: 'string', + default: '', + description: 'A username associated with the user (e.g. user_name.123)', + }, + { + displayName: 'Zip Code', + name: 'zip', + type: 'string', + default: '', + description: 'Zip code for the user', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ServiceNow/servicenow.svg b/packages/nodes-base/nodes/ServiceNow/servicenow.svg new file mode 100644 index 0000000000..e5567e562c --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/servicenow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1c6125e26e..7bbd5fc8f0 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -220,6 +220,7 @@ "dist/credentials/SentryIoApi.credentials.js", "dist/credentials/SentryIoServerApi.credentials.js", "dist/credentials/SentryIoOAuth2Api.credentials.js", + "dist/credentials/ServiceNowOAuth2Api.credentials.js", "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/Signl4Api.credentials.js", "dist/credentials/SlackApi.credentials.js", @@ -515,6 +516,7 @@ "dist/nodes/Set.node.js", "dist/nodes/SentryIo/SentryIo.node.js", "dist/nodes/SendGrid/SendGrid.node.js", + "dist/nodes/ServiceNow/ServiceNow.node.js", "dist/nodes/Shopify/Shopify.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Signl4/Signl4.node.js",