🔀 Merge branch 'RicardoE105-feature/jira-server-node'

This commit is contained in:
Jan Oberhauser 2020-02-06 19:04:06 -08:00
commit 0aea33bbc5
6 changed files with 285 additions and 39 deletions

View file

@ -24,6 +24,7 @@ export class JiraSoftwareCloudApi implements ICredentialType {
name: 'domain',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://example.atlassian.net',
},
];
}

View file

@ -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',
},
];
}

View file

@ -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<any> { // 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<any> { // 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 {

View file

@ -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:<br/>
*all Returns all fields.<br/>
*navigable Returns navigable fields.<br/>
Any issue field, prefixed with a minus to exclude.<br/>`,
},
{
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.<br/>
This parameter is useful where fields have been added by a connect app and a field's key<br/>
may differ from its ID.`,
},
{
displayName: ' JQL',
name: 'jql',
type: 'string',
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
description: 'A JQL expression.',
},
],
},
/* -------------------------------------------------------------------------- */
/* issue:changelog */
/* -------------------------------------------------------------------------- */
{

View file

@ -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<INodePropertyOptions[]> {
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;

View file

@ -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",