From c06934d9732bee9a31c71e2b52325a75465a2095 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 31 Jan 2020 10:21:14 -0500 Subject: [PATCH 1/5] :sparkles: Jira Software Server node --- .../JiraSoftwareServerApi.credentials.ts | 29 + .../nodes-base/nodes/Jira/GenericFunctions.ts | 54 +- .../nodes/Jira/JiraSoftwareServer.node.ts | 498 ++++++++++++++++++ packages/nodes-base/package.json | 2 + 4 files changed, 579 insertions(+), 4 deletions(-) create mode 100644 packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts diff --git a/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts new file mode 100644 index 0000000000..a5949fdc17 --- /dev/null +++ b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class JiraSoftwareServerApi implements ICredentialType { + name = 'jiraSoftwareServerApi'; + displayName = 'Jira SW Server API'; + properties = [ + { + displayName: 'Domain', + name: 'domain', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index b6c6b11674..ebf99b048e 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -42,12 +42,36 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut } } +export async function jiraSoftwareServerApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('jiraSoftwareServerApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const data = Buffer.from(`${credentials!.username}:${credentials!.password}`).toString(BINARY_ENCODING); + const headerWithAuthentication = Object.assign({}, + { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `${credentials.domain}/rest/api/2${endpoint}`, + body, + json: true + }; + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = + error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} -/** - * Make an API request to paginated intercom endpoint - * and return all results - */ export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; @@ -71,6 +95,28 @@ export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | return returnData; } +export async function jiraSoftwareServerApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.maxResults = 100; + + let uri: string | undefined; + + do { + responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query, uri); + uri = responseData.nextPage; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.isLast !== false && + responseData.nextPage !== undefined && + responseData.nextPage !== null + ); + + return returnData; +} export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any let result; diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts new file mode 100644 index 0000000000..9dfa2e4d3b --- /dev/null +++ b/packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts @@ -0,0 +1,498 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + jiraSoftwareServerApiRequest, + jiraSoftwareServerApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; +import { + issueOpeations, + issueFields, +} from './IssueDescription'; +import { + IIssue, + IFields, + INotify, + INotificationRecipients, + NotificationRecipientsRestrictions, + } from './IssueInterface'; + +export class JiraSoftwareServer implements INodeType { + description: INodeTypeDescription = { + displayName: 'Jira Software Server', + name: 'Jira Software Server', + icon: 'file:jira.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Jira Software Server API', + defaults: { + name: 'Jira Software Server', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'jiraSoftwareServerApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Issue', + value: 'issue', + description: 'Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask', + }, + ], + default: 'issue', + description: 'Resource to consume.', + }, + ...issueOpeations, + ...issueFields, + ], + }; + + methods = { + loadOptions: { + // Get all the projects to display them to user so that he can + // select them easily + async getProjects(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let projects; + try { + projects = await jiraSoftwareServerApiRequest.call(this, '/project/search', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const project of projects.values) { + const projectName = project.name; + const projectId = project.id; + + returnData.push({ + name: projectName, + value: projectId, + }); + } + return returnData; + }, + + // Get all the issue types to display them to user so that he can + // select them easily + async getIssueTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let issueTypes; + try { + issueTypes = await jiraSoftwareServerApiRequest.call(this, '/issuetype', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const issueType of issueTypes) { + const issueTypeName = issueType.name; + const issueTypeId = issueType.id; + + returnData.push({ + name: issueTypeName, + value: issueTypeId, + }); + } + return returnData; + }, + + // Get all the labels to display them to user so that he can + // select them easily + async getLabels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let labels; + try { + labels = await jiraSoftwareServerApiRequest.call(this, '/label', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const label of labels.values) { + const labelName = label; + const labelId = label; + + returnData.push({ + name: labelName, + value: labelId, + }); + } + return returnData; + }, + + // Get all the priorities to display them to user so that he can + // select them easily + async getPriorities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let priorities; + try { + priorities = await jiraSoftwareServerApiRequest.call(this, '/priority', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const priority of priorities) { + const priorityName = priority.name; + const priorityId = priority.id; + + returnData.push({ + name: priorityName, + value: priorityId, + }); + } + return returnData; + }, + + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let users; + try { + users = await jiraSoftwareServerApiRequest.call(this, '/users/search', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const user of users) { + const userName = user.displayName; + const userId = user.accountId; + + returnData.push({ + name: userName, + value: userId, + }); + } + return returnData; + }, + + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let groups; + try { + groups = await jiraSoftwareServerApiRequest.call(this, '/groups/picker', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const group of groups.groups) { + const groupName = group.name; + const groupId = group.name; + + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + } + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const 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++) { + if (resource === 'issue') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post + if (operation === 'create') { + const summary = this.getNodeParameter('summary', i) as string; + const projectId = this.getNodeParameter('project', i) as string; + const issueTypeId = this.getNodeParameter('issueType', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IIssue = {}; + const fields: IFields = { + summary, + project: { + id: projectId, + }, + issuetype: { + id: issueTypeId, + }, + }; + if (additionalFields.labels) { + fields.labels = additionalFields.labels as string[]; + } + if (additionalFields.priority) { + fields.priority = { + id: additionalFields.priority as string, + }; + } + if (additionalFields.assignee) { + fields.assignee = { + id: additionalFields.assignee as string, + }; + } + if (additionalFields.description) { + fields.description = additionalFields.description as string; + } + if (additionalFields.updateHistory) { + qs.updateHistory = additionalFields.updateHistory as boolean; + } + const issueTypes = await jiraSoftwareServerApiRequest.call(this, '/issuetype', 'GET', body, qs); + const subtaskIssues = []; + for (const issueType of issueTypes) { + if (issueType.subtask) { + subtaskIssues.push(issueType.id); + } + } + if (!additionalFields.parentIssueKey + && subtaskIssues.includes(issueTypeId)) { + throw new Error('You must define a Parent Issue Key when Issue type is sub-task'); + + } else if (additionalFields.parentIssueKey + && subtaskIssues.includes(issueTypeId)) { + fields.parent = { + key: (additionalFields.parentIssueKey as string).toUpperCase(), + }; + } + body.fields = fields; + try { + responseData = await jiraSoftwareServerApiRequest.call(this, '/issue', 'POST', body); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put + if (operation === 'update') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IIssue = {}; + const fields: IFields = {}; + if (updateFields.summary) { + fields.summary = updateFields.summary as string; + } + if (updateFields.issueType) { + fields.issuetype = { + id: updateFields.issueType as string, + }; + } + if (updateFields.labels) { + fields.labels = updateFields.labels as string[]; + } + if (updateFields.priority) { + fields.priority = { + id: updateFields.priority as string, + }; + } + if (updateFields.assignee) { + fields.assignee = { + id: updateFields.assignee as string, + }; + } + if (updateFields.description) { + fields.description = updateFields.description as string; + } + const issueTypes = await jiraSoftwareServerApiRequest.call(this, '/issuetype', 'GET', body); + const subtaskIssues = []; + for (const issueType of issueTypes) { + if (issueType.subtask) { + subtaskIssues.push(issueType.id); + } + } + if (!updateFields.parentIssueKey + && subtaskIssues.includes(updateFields.issueType)) { + throw new Error('You must define a Parent Issue Key when Issue type is sub-task'); + + } else if (updateFields.parentIssueKey + && subtaskIssues.includes(updateFields.issueType)) { + fields.parent = { + key: (updateFields.parentIssueKey as string).toUpperCase(), + }; + } + body.fields = fields; + try { + responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get + if (operation === 'get') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.fields) { + qs.fields = additionalFields.fields as string; + } + if (additionalFields.fieldsByKey) { + qs.fieldsByKey = additionalFields.fieldsByKey as boolean; + } + if (additionalFields.expand) { + qs.expand = additionalFields.expand as string; + } + if (additionalFields.properties) { + qs.properties = additionalFields.properties as string; + } + if (additionalFields.updateHistory) { + qs.updateHistory = additionalFields.updateHistory as string; + } + try { + responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}`, 'GET', {}, qs); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get + if (operation === 'changelog') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + try { + if (returnAll) { + responseData = await jiraSoftwareServerApiRequestAllItems.call(this, 'values',`/issue/${issueKey}/changelog`, 'GET'); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}/changelog`, 'GET', {}, qs); + responseData = responseData.values; + } + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post + if (operation === 'notify') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', 0) as boolean; + const body: INotify = {}; + if (additionalFields.textBody) { + body.textBody = additionalFields.textBody as string; + } + if (additionalFields.htmlBody) { + body.htmlBody = additionalFields.htmlBody as string; + } + if (!jsonActive) { + const notificationRecipientsValues = (this.getNodeParameter('notificationRecipientsUi', i) as IDataObject).notificationRecipientsValues as IDataObject[]; + const notificationRecipients: INotificationRecipients = {}; + if (notificationRecipientsValues) { + // @ts-ignore + if (notificationRecipientsValues.reporter) { + // @ts-ignore + notificationRecipients.reporter = notificationRecipientsValues.reporter as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.assignee) { + // @ts-ignore + notificationRecipients.assignee = notificationRecipientsValues.assignee as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.assignee) { + // @ts-ignore + notificationRecipients.watchers = notificationRecipientsValues.watchers as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.voters) { + // @ts-ignore + notificationRecipients.watchers = notificationRecipientsValues.voters as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.users.length > 0) { + // @ts-ignore + notificationRecipients.users = notificationRecipientsValues.users.map(user => { + return { + accountId: user + }; + }); + } + // @ts-ignore + if (notificationRecipientsValues.groups.length > 0) { + // @ts-ignore + notificationRecipients.groups = notificationRecipientsValues.groups.map(group => { + return { + name: group + }; + }); + } + } + body.to = notificationRecipients; + const notificationRecipientsRestrictionsValues = (this.getNodeParameter('notificationRecipientsRestrictionsUi', i) as IDataObject).notificationRecipientsRestrictionsValues as IDataObject[]; + const notificationRecipientsRestrictions: NotificationRecipientsRestrictions = {}; + if (notificationRecipientsRestrictionsValues) { + // @ts-ignore + if (notificationRecipientsRestrictionsValues.groups. length > 0) { + // @ts-ignore + notificationRecipientsRestrictions.groups = notificationRecipientsRestrictionsValues.groups.map(group => { + return { + name: group + }; + }); + } + } + body.restrict = notificationRecipientsRestrictions; + } else { + const notificationRecipientsJson = validateJSON(this.getNodeParameter('notificationRecipientsJson', i) as string); + if (notificationRecipientsJson) { + body.to = notificationRecipientsJson; + } + const notificationRecipientsRestrictionsJson = validateJSON(this.getNodeParameter('notificationRecipientsRestrictionsJson', i) as string); + if (notificationRecipientsRestrictionsJson) { + body.restrict = notificationRecipientsRestrictionsJson; + } + } + try { + responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}/notify`, 'POST', body, qs); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get + if (operation === 'transitions') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.transitionId) { + qs.transitionId = additionalFields.transitionId as string; + } + if (additionalFields.expand) { + qs.expand = additionalFields.expand as string; + } + if (additionalFields.skipRemoteOnlyCondition) { + qs.skipRemoteOnlyCondition = additionalFields.skipRemoteOnlyCondition as boolean; + } + try { + responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs); + responseData = responseData.transitions; + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete + if (operation === 'delete') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean; + qs.deleteSubtasks = deleteSubtasks; + try { + responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b59b37befd..820130645f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -58,6 +58,7 @@ "dist/credentials/Imap.credentials.js", "dist/credentials/IntercomApi.credentials.js", "dist/credentials/JiraSoftwareCloudApi.credentials.js", + "dist/credentials/JiraSoftwareServerApi.credentials.js", "dist/credentials/JotFormApi.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", @@ -149,6 +150,7 @@ "dist/nodes/Intercom/Intercom.node.js", "dist/nodes/Interval.node.js", "dist/nodes/Jira/JiraSoftwareCloud.node.js", + "dist/nodes/Jira/JiraSoftwareServer.node.js", "dist/nodes/JotForm/JotFormTrigger.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", From 94b13ddea2ac1534447b25171d5dc40ae7c9b2b0 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 1 Feb 2020 18:15:56 -0500 Subject: [PATCH 2/5] :zap: Support for Jira server --- .../JiraSoftwareCloudApi.credentials.ts | 56 +- .../JiraSoftwareServerApi.credentials.ts | 29 - .../nodes-base/nodes/Jira/GenericFunctions.ts | 55 +- .../nodes-base/nodes/Jira/IssueDescription.ts | 2 +- .../nodes/Jira/JiraSoftwareCloud.node.ts | 25 +- .../nodes/Jira/JiraSoftwareServer.node.ts | 498 ------------------ packages/nodes-base/package.json | 2 - 7 files changed, 73 insertions(+), 594 deletions(-) delete mode 100644 packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts delete mode 100644 packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts diff --git a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts index a11d526f7d..a40cac2bdc 100644 --- a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts +++ b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts @@ -5,23 +5,77 @@ import { export class JiraSoftwareCloudApi implements ICredentialType { name = 'jiraSoftwareCloudApi'; - displayName = 'Jira SW Cloud API'; + displayName = 'Jira SW API'; properties = [ + { + displayName: 'Jira Version', + name: 'jiraVersion', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'Cloud', + value: 'cloud', + }, + { + name: 'Server (Self Hosted)', + value: 'server', + }, + ], + default: 'cloud', + }, { displayName: 'Email', name: 'email', + displayOptions: { + show: { + jiraVersion: [ + 'cloud', + ], + }, + }, type: 'string' as NodePropertyTypes, default: '', }, { displayName: 'API Token', name: 'apiToken', + displayOptions: { + show: { + jiraVersion: [ + 'cloud', + ], + }, + }, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + displayOptions: { + show: { + jiraVersion: [ + 'server', + ], + }, + }, + typeOptions: { + password: true, + }, type: 'string' as NodePropertyTypes, default: '', }, { displayName: 'Domain', name: 'domain', + displayOptions: { + show: { + jiraVersion: [ + 'cloud', + 'server', + ], + }, + }, type: 'string' as NodePropertyTypes, default: '', }, diff --git a/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts deleted file mode 100644 index a5949fdc17..0000000000 --- a/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - ICredentialType, - NodePropertyTypes, -} from 'n8n-workflow'; - -export class JiraSoftwareServerApi implements ICredentialType { - name = 'jiraSoftwareServerApi'; - displayName = 'Jira SW Server API'; - properties = [ - { - displayName: 'Domain', - name: 'domain', - type: 'string' as NodePropertyTypes, - default: '', - }, - { - displayName: 'Username', - name: 'username', - type: 'string' as NodePropertyTypes, - default: '', - }, - { - displayName: 'Password', - name: 'password', - type: 'string' as NodePropertyTypes, - default: '', - }, - ]; -} diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index ebf99b048e..d1ea0983b1 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -17,37 +17,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const data = Buffer.from(`${credentials!.email}:${credentials!.apiToken}`).toString(BINARY_ENCODING); - const headerWithAuthentication = Object.assign({}, - { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); - const options: OptionsWithUri = { - headers: headerWithAuthentication, - method, - qs: query, - uri: uri || `${credentials.domain}/rest/api/2${endpoint}`, - body, - json: true - }; - - try { - return await this.helpers.request!(options); - } catch (error) { - const errorMessage = - error.response.body.message || error.response.body.Message; - - if (errorMessage !== undefined) { - throw errorMessage; - } - throw error.response.body; - } -} - -export async function jiraSoftwareServerApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('jiraSoftwareServerApi'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - const data = Buffer.from(`${credentials!.username}:${credentials!.password}`).toString(BINARY_ENCODING); + const data = Buffer.from(`${credentials!.email}:${(credentials.jiraVersion === 'server') ? credentials.password : credentials.apiKey }`).toString(BINARY_ENCODING); const headerWithAuthentication = Object.assign({}, { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); const options: OptionsWithUri = { @@ -95,29 +65,6 @@ export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | return returnData; } -export async function jiraSoftwareServerApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any - - const returnData: IDataObject[] = []; - - let responseData; - - query.maxResults = 100; - - let uri: string | undefined; - - do { - responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query, uri); - uri = responseData.nextPage; - returnData.push.apply(returnData, responseData[propertyName]); - } while ( - responseData.isLast !== false && - responseData.nextPage !== undefined && - responseData.nextPage !== null - ); - - return returnData; -} - export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any let result; try { diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index 62558a6bf5..f185557f5f 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties } from "n8n-workflow"; -export const issueOpeations = [ +export const issueOperations = [ { displayName: 'Operation', name: 'operation', diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts index f5dd4afe85..689c902176 100644 --- a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts @@ -15,7 +15,7 @@ import { validateJSON, } from './GenericFunctions'; import { - issueOpeations, + issueOperations, issueFields, } from './IssueDescription'; import { @@ -28,15 +28,15 @@ import { export class JiraSoftwareCloud implements INodeType { description: INodeTypeDescription = { - displayName: 'Jira Software Cloud', + displayName: 'Jira Software', name: 'Jira Software Cloud', icon: 'file:jira.png', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Jira Software Cloud API', + description: 'Consume Jira Software API', defaults: { - name: 'Jira Software Cloud', + name: 'Jira Software', color: '#c02428', }, inputs: ['main'], @@ -45,7 +45,7 @@ export class JiraSoftwareCloud implements INodeType { { name: 'jiraSoftwareCloudApi', required: true, - } + }, ], properties: [ { @@ -62,7 +62,7 @@ export class JiraSoftwareCloud implements INodeType { default: 'issue', description: 'Resource to consume.', }, - ...issueOpeations, + ...issueOperations, ...issueFields, ], }; @@ -73,16 +73,23 @@ export class JiraSoftwareCloud implements INodeType { // select them easily async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; + const credentials = this.getCredentials('jiraSoftwareCloudApi'); let projects; + let endpoint = '/project/search'; + if (credentials!.jiraVersion === 'server') { + endpoint = '/project'; + } try { - projects = await jiraSoftwareCloudApiRequest.call(this, '/project/search', 'GET'); + projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); } catch (err) { throw new Error(`Jira Error: ${err}`); } - for (const project of projects.values) { + if (projects.values && Array.isArray(projects.values)) { + projects = projects.values; + } + for (const project of projects) { const projectName = project.name; const projectId = project.id; - returnData.push({ name: projectName, value: projectId, diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts deleted file mode 100644 index 9dfa2e4d3b..0000000000 --- a/packages/nodes-base/nodes/Jira/JiraSoftwareServer.node.ts +++ /dev/null @@ -1,498 +0,0 @@ -import { - IExecuteFunctions, -} from 'n8n-core'; -import { - IDataObject, - INodeTypeDescription, - INodeExecutionData, - INodeType, - ILoadOptionsFunctions, - INodePropertyOptions, -} from 'n8n-workflow'; -import { - jiraSoftwareServerApiRequest, - jiraSoftwareServerApiRequestAllItems, - validateJSON, -} from './GenericFunctions'; -import { - issueOpeations, - issueFields, -} from './IssueDescription'; -import { - IIssue, - IFields, - INotify, - INotificationRecipients, - NotificationRecipientsRestrictions, - } from './IssueInterface'; - -export class JiraSoftwareServer implements INodeType { - description: INodeTypeDescription = { - displayName: 'Jira Software Server', - name: 'Jira Software Server', - icon: 'file:jira.png', - group: ['output'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Jira Software Server API', - defaults: { - name: 'Jira Software Server', - color: '#c02428', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'jiraSoftwareServerApi', - required: true, - } - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - options: [ - { - name: 'Issue', - value: 'issue', - description: 'Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask', - }, - ], - default: 'issue', - description: 'Resource to consume.', - }, - ...issueOpeations, - ...issueFields, - ], - }; - - methods = { - loadOptions: { - // Get all the projects to display them to user so that he can - // select them easily - async getProjects(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let projects; - try { - projects = await jiraSoftwareServerApiRequest.call(this, '/project/search', 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } - for (const project of projects.values) { - const projectName = project.name; - const projectId = project.id; - - returnData.push({ - name: projectName, - value: projectId, - }); - } - return returnData; - }, - - // Get all the issue types to display them to user so that he can - // select them easily - async getIssueTypes(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let issueTypes; - try { - issueTypes = await jiraSoftwareServerApiRequest.call(this, '/issuetype', 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } - for (const issueType of issueTypes) { - const issueTypeName = issueType.name; - const issueTypeId = issueType.id; - - returnData.push({ - name: issueTypeName, - value: issueTypeId, - }); - } - return returnData; - }, - - // Get all the labels to display them to user so that he can - // select them easily - async getLabels(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let labels; - try { - labels = await jiraSoftwareServerApiRequest.call(this, '/label', 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } - for (const label of labels.values) { - const labelName = label; - const labelId = label; - - returnData.push({ - name: labelName, - value: labelId, - }); - } - return returnData; - }, - - // Get all the priorities to display them to user so that he can - // select them easily - async getPriorities(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let priorities; - try { - priorities = await jiraSoftwareServerApiRequest.call(this, '/priority', 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } - for (const priority of priorities) { - const priorityName = priority.name; - const priorityId = priority.id; - - returnData.push({ - name: priorityName, - value: priorityId, - }); - } - return returnData; - }, - - // Get all the users to display them to user so that he can - // select them easily - async getUsers(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let users; - try { - users = await jiraSoftwareServerApiRequest.call(this, '/users/search', 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } - for (const user of users) { - const userName = user.displayName; - const userId = user.accountId; - - returnData.push({ - name: userName, - value: userId, - }); - } - return returnData; - }, - - // Get all the groups to display them to user so that he can - // select them easily - async getGroups(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let groups; - try { - groups = await jiraSoftwareServerApiRequest.call(this, '/groups/picker', 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } - for (const group of groups.groups) { - const groupName = group.name; - const groupId = group.name; - - returnData.push({ - name: groupName, - value: groupId, - }); - } - return returnData; - } - } - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: IDataObject[] = []; - const length = items.length as unknown as number; - let responseData; - const 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++) { - if (resource === 'issue') { - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post - if (operation === 'create') { - const summary = this.getNodeParameter('summary', i) as string; - const projectId = this.getNodeParameter('project', i) as string; - const issueTypeId = this.getNodeParameter('issueType', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const body: IIssue = {}; - const fields: IFields = { - summary, - project: { - id: projectId, - }, - issuetype: { - id: issueTypeId, - }, - }; - if (additionalFields.labels) { - fields.labels = additionalFields.labels as string[]; - } - if (additionalFields.priority) { - fields.priority = { - id: additionalFields.priority as string, - }; - } - if (additionalFields.assignee) { - fields.assignee = { - id: additionalFields.assignee as string, - }; - } - if (additionalFields.description) { - fields.description = additionalFields.description as string; - } - if (additionalFields.updateHistory) { - qs.updateHistory = additionalFields.updateHistory as boolean; - } - const issueTypes = await jiraSoftwareServerApiRequest.call(this, '/issuetype', 'GET', body, qs); - const subtaskIssues = []; - for (const issueType of issueTypes) { - if (issueType.subtask) { - subtaskIssues.push(issueType.id); - } - } - if (!additionalFields.parentIssueKey - && subtaskIssues.includes(issueTypeId)) { - throw new Error('You must define a Parent Issue Key when Issue type is sub-task'); - - } else if (additionalFields.parentIssueKey - && subtaskIssues.includes(issueTypeId)) { - fields.parent = { - key: (additionalFields.parentIssueKey as string).toUpperCase(), - }; - } - body.fields = fields; - try { - responseData = await jiraSoftwareServerApiRequest.call(this, '/issue', 'POST', body); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put - if (operation === 'update') { - const issueKey = this.getNodeParameter('issueKey', i) as string; - const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; - const body: IIssue = {}; - const fields: IFields = {}; - if (updateFields.summary) { - fields.summary = updateFields.summary as string; - } - if (updateFields.issueType) { - fields.issuetype = { - id: updateFields.issueType as string, - }; - } - if (updateFields.labels) { - fields.labels = updateFields.labels as string[]; - } - if (updateFields.priority) { - fields.priority = { - id: updateFields.priority as string, - }; - } - if (updateFields.assignee) { - fields.assignee = { - id: updateFields.assignee as string, - }; - } - if (updateFields.description) { - fields.description = updateFields.description as string; - } - const issueTypes = await jiraSoftwareServerApiRequest.call(this, '/issuetype', 'GET', body); - const subtaskIssues = []; - for (const issueType of issueTypes) { - if (issueType.subtask) { - subtaskIssues.push(issueType.id); - } - } - if (!updateFields.parentIssueKey - && subtaskIssues.includes(updateFields.issueType)) { - throw new Error('You must define a Parent Issue Key when Issue type is sub-task'); - - } else if (updateFields.parentIssueKey - && subtaskIssues.includes(updateFields.issueType)) { - fields.parent = { - key: (updateFields.parentIssueKey as string).toUpperCase(), - }; - } - body.fields = fields; - try { - responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get - if (operation === 'get') { - const issueKey = this.getNodeParameter('issueKey', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - if (additionalFields.fields) { - qs.fields = additionalFields.fields as string; - } - if (additionalFields.fieldsByKey) { - qs.fieldsByKey = additionalFields.fieldsByKey as boolean; - } - if (additionalFields.expand) { - qs.expand = additionalFields.expand as string; - } - if (additionalFields.properties) { - qs.properties = additionalFields.properties as string; - } - if (additionalFields.updateHistory) { - qs.updateHistory = additionalFields.updateHistory as string; - } - try { - responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}`, 'GET', {}, qs); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get - if (operation === 'changelog') { - const issueKey = this.getNodeParameter('issueKey', i) as string; - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - try { - if (returnAll) { - responseData = await jiraSoftwareServerApiRequestAllItems.call(this, 'values',`/issue/${issueKey}/changelog`, 'GET'); - } else { - qs.maxResults = this.getNodeParameter('limit', i) as number; - responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}/changelog`, 'GET', {}, qs); - responseData = responseData.values; - } - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post - if (operation === 'notify') { - const issueKey = this.getNodeParameter('issueKey', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const jsonActive = this.getNodeParameter('jsonParameters', 0) as boolean; - const body: INotify = {}; - if (additionalFields.textBody) { - body.textBody = additionalFields.textBody as string; - } - if (additionalFields.htmlBody) { - body.htmlBody = additionalFields.htmlBody as string; - } - if (!jsonActive) { - const notificationRecipientsValues = (this.getNodeParameter('notificationRecipientsUi', i) as IDataObject).notificationRecipientsValues as IDataObject[]; - const notificationRecipients: INotificationRecipients = {}; - if (notificationRecipientsValues) { - // @ts-ignore - if (notificationRecipientsValues.reporter) { - // @ts-ignore - notificationRecipients.reporter = notificationRecipientsValues.reporter as boolean; - } - // @ts-ignore - if (notificationRecipientsValues.assignee) { - // @ts-ignore - notificationRecipients.assignee = notificationRecipientsValues.assignee as boolean; - } - // @ts-ignore - if (notificationRecipientsValues.assignee) { - // @ts-ignore - notificationRecipients.watchers = notificationRecipientsValues.watchers as boolean; - } - // @ts-ignore - if (notificationRecipientsValues.voters) { - // @ts-ignore - notificationRecipients.watchers = notificationRecipientsValues.voters as boolean; - } - // @ts-ignore - if (notificationRecipientsValues.users.length > 0) { - // @ts-ignore - notificationRecipients.users = notificationRecipientsValues.users.map(user => { - return { - accountId: user - }; - }); - } - // @ts-ignore - if (notificationRecipientsValues.groups.length > 0) { - // @ts-ignore - notificationRecipients.groups = notificationRecipientsValues.groups.map(group => { - return { - name: group - }; - }); - } - } - body.to = notificationRecipients; - const notificationRecipientsRestrictionsValues = (this.getNodeParameter('notificationRecipientsRestrictionsUi', i) as IDataObject).notificationRecipientsRestrictionsValues as IDataObject[]; - const notificationRecipientsRestrictions: NotificationRecipientsRestrictions = {}; - if (notificationRecipientsRestrictionsValues) { - // @ts-ignore - if (notificationRecipientsRestrictionsValues.groups. length > 0) { - // @ts-ignore - notificationRecipientsRestrictions.groups = notificationRecipientsRestrictionsValues.groups.map(group => { - return { - name: group - }; - }); - } - } - body.restrict = notificationRecipientsRestrictions; - } else { - const notificationRecipientsJson = validateJSON(this.getNodeParameter('notificationRecipientsJson', i) as string); - if (notificationRecipientsJson) { - body.to = notificationRecipientsJson; - } - const notificationRecipientsRestrictionsJson = validateJSON(this.getNodeParameter('notificationRecipientsRestrictionsJson', i) as string); - if (notificationRecipientsRestrictionsJson) { - body.restrict = notificationRecipientsRestrictionsJson; - } - } - try { - responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}/notify`, 'POST', body, qs); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get - if (operation === 'transitions') { - const issueKey = this.getNodeParameter('issueKey', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - if (additionalFields.transitionId) { - qs.transitionId = additionalFields.transitionId as string; - } - if (additionalFields.expand) { - qs.expand = additionalFields.expand as string; - } - if (additionalFields.skipRemoteOnlyCondition) { - qs.skipRemoteOnlyCondition = additionalFields.skipRemoteOnlyCondition as boolean; - } - try { - responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs); - responseData = responseData.transitions; - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete - if (operation === 'delete') { - const issueKey = this.getNodeParameter('issueKey', i) as string; - const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean; - qs.deleteSubtasks = deleteSubtasks; - try { - responseData = await jiraSoftwareServerApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } - } - } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else { - returnData.push(responseData as IDataObject); - } - } - return [this.helpers.returnJsonArray(returnData)]; - } -} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 820130645f..b59b37befd 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -58,7 +58,6 @@ "dist/credentials/Imap.credentials.js", "dist/credentials/IntercomApi.credentials.js", "dist/credentials/JiraSoftwareCloudApi.credentials.js", - "dist/credentials/JiraSoftwareServerApi.credentials.js", "dist/credentials/JotFormApi.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", @@ -150,7 +149,6 @@ "dist/nodes/Intercom/Intercom.node.js", "dist/nodes/Interval.node.js", "dist/nodes/Jira/JiraSoftwareCloud.node.js", - "dist/nodes/Jira/JiraSoftwareServer.node.js", "dist/nodes/JotForm/JotFormTrigger.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", From 7d5bbadc9b555cc0fc4223f1e24872b24a63fa3f Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 1 Feb 2020 18:20:23 -0500 Subject: [PATCH 3/5] :zap: small fix --- packages/nodes-base/nodes/Jira/GenericFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index d1ea0983b1..fb8473d27c 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -17,7 +17,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const data = Buffer.from(`${credentials!.email}:${(credentials.jiraVersion === 'server') ? credentials.password : credentials.apiKey }`).toString(BINARY_ENCODING); + const data = Buffer.from(`${credentials!.email}:${(credentials.jiraVersion === 'server') ? credentials.password : credentials.apiToken }`).toString(BINARY_ENCODING); const headerWithAuthentication = Object.assign({}, { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); const options: OptionsWithUri = { From 960d439cbcaf1e74a3539b3a60b6522844917936 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 2 Feb 2020 10:01:56 -0500 Subject: [PATCH 4/5] :zap: fixes --- .../JiraSoftwareCloudApi.credentials.ts | 56 +------ .../JiraSoftwareServerApi.credentials.ts | 32 ++++ .../nodes-base/nodes/Jira/GenericFunctions.ts | 41 +++-- .../nodes-base/nodes/Jira/IssueDescription.ts | 147 ++++++++++++++++++ .../nodes/Jira/JiraSoftwareCloud.node.ts | 61 +++++++- packages/nodes-base/package.json | 1 + 6 files changed, 264 insertions(+), 74 deletions(-) create mode 100644 packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts diff --git a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts index a40cac2bdc..a11d526f7d 100644 --- a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts +++ b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts @@ -5,77 +5,23 @@ import { export class JiraSoftwareCloudApi implements ICredentialType { name = 'jiraSoftwareCloudApi'; - displayName = 'Jira SW API'; + displayName = 'Jira SW Cloud API'; properties = [ - { - displayName: 'Jira Version', - name: 'jiraVersion', - type: 'options' as NodePropertyTypes, - options: [ - { - name: 'Cloud', - value: 'cloud', - }, - { - name: 'Server (Self Hosted)', - value: 'server', - }, - ], - default: 'cloud', - }, { displayName: 'Email', name: 'email', - displayOptions: { - show: { - jiraVersion: [ - 'cloud', - ], - }, - }, type: 'string' as NodePropertyTypes, default: '', }, { displayName: 'API Token', name: 'apiToken', - displayOptions: { - show: { - jiraVersion: [ - 'cloud', - ], - }, - }, - type: 'string' as NodePropertyTypes, - default: '', - }, - { - displayName: 'Password', - name: 'password', - displayOptions: { - show: { - jiraVersion: [ - 'server', - ], - }, - }, - typeOptions: { - password: true, - }, type: 'string' as NodePropertyTypes, default: '', }, { displayName: 'Domain', name: 'domain', - displayOptions: { - show: { - jiraVersion: [ - 'cloud', - 'server', - ], - }, - }, type: 'string' as NodePropertyTypes, default: '', }, diff --git a/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts new file mode 100644 index 0000000000..47d94eb6f0 --- /dev/null +++ b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts @@ -0,0 +1,32 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class JiraSoftwareServerApi implements ICredentialType { + name = 'jiraSoftwareServerApi'; + displayName = 'Jira SW Server API'; + properties = [ + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + typeOptions: { + password: true, + }, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index fb8473d27c..421f508193 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -13,18 +13,27 @@ import { } from 'n8n-workflow'; export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('jiraSoftwareCloudApi'); - if (credentials === undefined) { + let data; let domain; + const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi'); + const jiraServerCredentials = this.getCredentials('jiraSoftwareServerApi'); + if (jiraCloudCredentials === undefined + && jiraServerCredentials === undefined) { throw new Error('No credentials got returned!'); } - const data = Buffer.from(`${credentials!.email}:${(credentials.jiraVersion === 'server') ? credentials.password : credentials.apiToken }`).toString(BINARY_ENCODING); + if (jiraCloudCredentials !== undefined) { + domain = jiraCloudCredentials!.domain; + data = Buffer.from(`${jiraCloudCredentials!.email}:${jiraCloudCredentials!.apiToken}`).toString(BINARY_ENCODING); + } else { + domain = jiraServerCredentials!.domain; + data = Buffer.from(`${jiraServerCredentials!.email}:${jiraServerCredentials!.password}`).toString(BINARY_ENCODING); + } const headerWithAuthentication = Object.assign({}, { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); const options: OptionsWithUri = { headers: headerWithAuthentication, method, qs: query, - uri: uri || `${credentials.domain}/rest/api/2${endpoint}`, + uri: uri || `${domain}/rest/api/2${endpoint}`, body, json: true }; @@ -32,13 +41,11 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = - error.response.body.message || error.response.body.Message; - - if (errorMessage !== undefined) { - throw errorMessage; + let errorMessage = error; + if (error.error && error.error.errorMessages) { + errorMessage = error.error.errorMessages; } - throw error.response.body; + throw new Error(errorMessage); } } @@ -48,18 +55,18 @@ export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | let responseData; + query.startAt = 0; + body.startAt = 0; query.maxResults = 100; - - let uri: string | undefined; + body.maxResults = 100; do { - responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query, uri); - uri = responseData.nextPage; + responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query); returnData.push.apply(returnData, responseData[propertyName]); + query.startAt = responseData.startAt + responseData.maxResults; + body.startAt = responseData.startAt + responseData.maxResults; } while ( - responseData.isLast !== false && - responseData.nextPage !== undefined && - responseData.nextPage !== null + (responseData.startAt + responseData.maxResults < responseData.total) ); return returnData; diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index f185557f5f..3712f9f047 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -28,6 +28,11 @@ export const issueOperations = [ value: 'get', description: 'Get an issue', }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all issues', + }, { name: 'Changelog', value: 'changelog', @@ -452,6 +457,148 @@ export const issueFields = [ }, /* -------------------------------------------------------------------------- */ +/* issue:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'issue', + ], + }, + }, + default: {}, + options: [ + { + displayName: ' JQL', + name: 'jql', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A JQL expression.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '*navigable', + description: `A list of fields to return for each issue, use it to retrieve a subset of fields. This parameter accepts a comma-separated list. Expand options include:
+ *all Returns all fields.
+ *navigable Returns navigable fields.
+ Any issue field, prefixed with a minus to exclude.
`, + }, + { + displayName: 'Expand', + name: 'expand', + type: 'options', + default: '', + options: [ + { + name: 'Rendered Fields', + valie: 'renderedFields', + description: ' Returns field values rendered in HTML format.', + }, + { + name: 'Names', + valie: 'names', + description: 'Returns the display name of each field', + }, + { + name: 'Schema', + valie: 'schema', + description: 'Returns the schema describing a field type.', + }, + { + name: 'Transitions', + valie: 'transitions', + description: ' Returns all possible transitions for the issue.', + }, + { + name: 'Operations', + valie: 'operations', + description: 'Returns all possible operations for the issue.', + }, + { + name: 'Editmeta', + valie: 'editmeta', + description: 'Returns information about how each field can be edited', + }, + { + name: 'Changelog', + valie: 'changelog', + description: 'Returns a list of recent updates to an issue, sorted by date, starting from the most recent.', + }, + { + name: 'Versioned Representations', + valie: 'versionedRepresentations', + description: `JSON array containing each version of a field's value`, + }, + ], + description: `Use expand to include additional information about issues in the response`, + }, + { + displayName: 'Fields By Key', + name: 'fieldsByKey', + type: 'boolean', + required: false, + default: false, + description: `Indicates whether fields in fields are referenced by keys rather than IDs.
+ This parameter is useful where fields have been added by a connect app and a field's key
+ may differ from its ID.`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ /* issue:changelog */ /* -------------------------------------------------------------------------- */ { diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts index 689c902176..b4e3cf0c2a 100644 --- a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts @@ -45,9 +45,43 @@ export class JiraSoftwareCloud implements INodeType { { name: 'jiraSoftwareCloudApi', required: true, + displayOptions: { + show: { + jiraVersion: [ + 'cloud', + ], + }, + }, + }, + { + name: 'jiraSoftwareServerApi', + required: true, + displayOptions: { + show: { + jiraVersion: [ + 'server', + ], + }, + }, }, ], properties: [ + { + displayName: 'Jira Version', + name: 'jiraVersion', + type: 'options', + options: [ + { + name: 'Cloud', + value: 'cloud', + }, + { + name: 'Server (Self Hosted)', + value: 'server', + }, + ], + default: 'cloud', + }, { displayName: 'Resource', name: 'resource', @@ -73,10 +107,10 @@ export class JiraSoftwareCloud implements INodeType { // select them easily async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const credentials = this.getCredentials('jiraSoftwareCloudApi'); + const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi'); let projects; let endpoint = '/project/search'; - if (credentials!.jiraVersion === 'server') { + if (jiraCloudCredentials === undefined) { endpoint = '/project'; } try { @@ -360,6 +394,29 @@ export class JiraSoftwareCloud implements INodeType { throw new Error(`Jira Error: ${JSON.stringify(err)}`); } } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.fields) { + body.fields = (options.fields as string).split(',') as string[]; + } + if (options.jql) { + body.jql = options.jql as string; + } + if (options.expand) { + body.expand = options.expand as string; + } + if (returnAll) { + responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'issues', `/search`, 'POST', body); + } else { + const limit = this.getNodeParameter('limit', i) as number; + body.maxResults = limit; + responseData = await jiraSoftwareCloudApiRequest.call(this, `/search`, 'POST', body); + responseData = responseData.issues; + } + } //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get if (operation === 'changelog') { const issueKey = this.getNodeParameter('issueKey', i) as string; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b59b37befd..2838e86573 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -58,6 +58,7 @@ "dist/credentials/Imap.credentials.js", "dist/credentials/IntercomApi.credentials.js", "dist/credentials/JiraSoftwareCloudApi.credentials.js", + "dist/credentials/JiraSoftwareServerApi.credentials.js", "dist/credentials/JotFormApi.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", From 201dd95c90e1588a230935401f1775d317489cd8 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 6 Feb 2020 19:03:29 -0800 Subject: [PATCH 5/5] :zap: Small improvements on Jira-Node --- .../JiraSoftwareCloudApi.credentials.ts | 1 + .../JiraSoftwareServerApi.credentials.ts | 1 + .../nodes-base/nodes/Jira/GenericFunctions.ts | 18 +-- .../nodes-base/nodes/Jira/IssueDescription.ts | 112 +++++++++--------- 4 files changed, 67 insertions(+), 65 deletions(-) diff --git a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts index a11d526f7d..430a21163b 100644 --- a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts +++ b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts @@ -24,6 +24,7 @@ export class JiraSoftwareCloudApi implements ICredentialType { name: 'domain', type: 'string' as NodePropertyTypes, default: '', + placeholder: 'https://example.atlassian.net', }, ]; } diff --git a/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts index 47d94eb6f0..73b4b2a97b 100644 --- a/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts +++ b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts @@ -27,6 +27,7 @@ export class JiraSoftwareServerApi implements ICredentialType { name: 'domain', type: 'string' as NodePropertyTypes, default: '', + placeholder: 'https://example.com', }, ]; } diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 421f508193..66ca437ded 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -2,10 +2,9 @@ import { OptionsWithUri } from 'request'; import { IExecuteFunctions, + IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, - IExecuteSingleFunctions, - BINARY_ENCODING } from 'n8n-core'; import { @@ -16,21 +15,22 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut let data; let domain; const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi'); const jiraServerCredentials = this.getCredentials('jiraSoftwareServerApi'); - if (jiraCloudCredentials === undefined - && jiraServerCredentials === undefined) { + if (jiraCloudCredentials === undefined && jiraServerCredentials === undefined) { throw new Error('No credentials got returned!'); } if (jiraCloudCredentials !== undefined) { domain = jiraCloudCredentials!.domain; - data = Buffer.from(`${jiraCloudCredentials!.email}:${jiraCloudCredentials!.apiToken}`).toString(BINARY_ENCODING); + data = Buffer.from(`${jiraCloudCredentials!.email}:${jiraCloudCredentials!.apiToken}`).toString('base64'); } else { domain = jiraServerCredentials!.domain; - data = Buffer.from(`${jiraServerCredentials!.email}:${jiraServerCredentials!.password}`).toString(BINARY_ENCODING); + data = Buffer.from(`${jiraServerCredentials!.email}:${jiraServerCredentials!.password}`).toString('base64'); } - const headerWithAuthentication = Object.assign({}, - { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); const options: OptionsWithUri = { - headers: headerWithAuthentication, + headers: { + Authorization: `Basic ${data}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, method, qs: query, uri: uri || `${domain}/rest/api/2${endpoint}`, diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index 3712f9f047..ae46db23e3 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -518,14 +518,53 @@ export const issueFields = [ default: {}, options: [ { - displayName: ' JQL', - name: 'jql', - type: 'string', + displayName: 'Expand', + name: 'expand', + type: 'options', default: '', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'A JQL expression.', + options: [ + { + name: 'Changelog', + value: 'changelog', + description: 'Returns a list of recent updates to an issue, sorted by date, starting from the most recent.', + }, + { + name: 'Editmeta', + value: 'editmeta', + description: 'Returns information about how each field can be edited', + }, + { + name: 'Names', + value: 'names', + description: 'Returns the display name of each field', + }, + { + name: 'Operations', + value: 'operations', + description: 'Returns all possible operations for the issue.', + }, + { + name: 'Rendered Fields', + value: 'renderedFields', + description: ' Returns field values rendered in HTML format.', + }, + { + name: 'Schema', + value: 'schema', + description: 'Returns the schema describing a field type.', + }, + { + name: 'Transitions', + value: 'transitions', + description: ' Returns all possible transitions for the issue.', + }, + { + name: 'Versioned Representations', + value: 'versionedRepresentations', + description: `JSON array containing each version of a field's value`, + }, + ], + description: `Use expand to include additional information about issues in the response`, }, { displayName: 'Fields', @@ -537,55 +576,6 @@ export const issueFields = [ *navigable Returns navigable fields.
Any issue field, prefixed with a minus to exclude.
`, }, - { - displayName: 'Expand', - name: 'expand', - type: 'options', - default: '', - options: [ - { - name: 'Rendered Fields', - valie: 'renderedFields', - description: ' Returns field values rendered in HTML format.', - }, - { - name: 'Names', - valie: 'names', - description: 'Returns the display name of each field', - }, - { - name: 'Schema', - valie: 'schema', - description: 'Returns the schema describing a field type.', - }, - { - name: 'Transitions', - valie: 'transitions', - description: ' Returns all possible transitions for the issue.', - }, - { - name: 'Operations', - valie: 'operations', - description: 'Returns all possible operations for the issue.', - }, - { - name: 'Editmeta', - valie: 'editmeta', - description: 'Returns information about how each field can be edited', - }, - { - name: 'Changelog', - valie: 'changelog', - description: 'Returns a list of recent updates to an issue, sorted by date, starting from the most recent.', - }, - { - name: 'Versioned Representations', - valie: 'versionedRepresentations', - description: `JSON array containing each version of a field's value`, - }, - ], - description: `Use expand to include additional information about issues in the response`, - }, { displayName: 'Fields By Key', name: 'fieldsByKey', @@ -596,6 +586,16 @@ export const issueFields = [ This parameter is useful where fields have been added by a connect app and a field's key
may differ from its ID.`, }, + { + displayName: ' JQL', + name: 'jql', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A JQL expression.', + }, ], }, /* -------------------------------------------------------------------------- */