diff --git a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts index a11d526f7..430a21163 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 new file mode 100644 index 000000000..73b4b2a97 --- /dev/null +++ b/packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts @@ -0,0 +1,33 @@ +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: '', + placeholder: 'https://example.com', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index b6c6b1167..66ca437de 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 { @@ -13,18 +12,28 @@ 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!.apiToken}`).toString(BINARY_ENCODING); - const headerWithAuthentication = Object.assign({}, - { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); + if (jiraCloudCredentials !== undefined) { + domain = jiraCloudCredentials!.domain; + data = Buffer.from(`${jiraCloudCredentials!.email}:${jiraCloudCredentials!.apiToken}`).toString('base64'); + } else { + domain = jiraServerCredentials!.domain; + data = Buffer.from(`${jiraServerCredentials!.email}:${jiraServerCredentials!.password}`).toString('base64'); + } const options: OptionsWithUri = { - headers: headerWithAuthentication, + headers: { + Authorization: `Basic ${data}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, method, qs: query, - uri: uri || `${credentials.domain}/rest/api/2${endpoint}`, + uri: uri || `${domain}/rest/api/2${endpoint}`, body, json: true }; @@ -32,46 +41,37 @@ 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); } } - - -/** - * 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[] = []; 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; } - 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 62558a6bf..ae46db23e 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', @@ -28,6 +28,11 @@ export const issueOpeations = [ 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: 'Expand', + name: 'expand', + type: 'options', + default: '', + 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', + 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: '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.`, + }, + { + displayName: ' JQL', + name: 'jql', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A JQL expression.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ /* issue:changelog */ /* -------------------------------------------------------------------------- */ { diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts index f5dd4afe8..b4e3cf0c2 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,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', @@ -62,7 +96,7 @@ export class JiraSoftwareCloud implements INodeType { default: 'issue', description: 'Resource to consume.', }, - ...issueOpeations, + ...issueOperations, ...issueFields, ], }; @@ -73,16 +107,23 @@ export class JiraSoftwareCloud implements INodeType { // select them easily async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; + const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi'); let projects; + let endpoint = '/project/search'; + if (jiraCloudCredentials === undefined) { + 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, @@ -353,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 b59b37bef..2838e8657 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",