diff --git a/packages/nodes-base/credentials/TravisCiApi.credentials.ts b/packages/nodes-base/credentials/TravisCiApi.credentials.ts new file mode 100644 index 0000000000..21dab91866 --- /dev/null +++ b/packages/nodes-base/credentials/TravisCiApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TravisCiApi implements ICredentialType { + name = 'travisCiApi'; + displayName = 'Travis API'; + properties = [ + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/TravisCi/BuildDescription.ts b/packages/nodes-base/nodes/TravisCi/BuildDescription.ts new file mode 100644 index 0000000000..7c6a18e7dc --- /dev/null +++ b/packages/nodes-base/nodes/TravisCi/BuildDescription.ts @@ -0,0 +1,351 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const buildOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'build', + ], + }, + }, + options: [ + { + name: 'Cancel', + value: 'cancel', + description: 'Cancel a build', + }, + { + name: 'Get', + value: 'get', + description: 'Get a build', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all builds', + }, + { + name: 'Restart', + value: 'restart', + description: 'Restart a build', + }, + { + name: 'Trigger', + value: 'trigger', + description: 'Trigger a build', + }, + ], + default: 'cancel', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const buildFields = [ + +/* -------------------------------------------------------------------------- */ +/* build:cancel */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Build ID', + name: 'buildId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'cancel', + ], + resource: [ + 'build', + ], + }, + }, + default: '', + description: 'Value uniquely identifying the build.', + }, +/* -------------------------------------------------------------------------- */ +/* build:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Build ID', + name: 'buildId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'build', + ], + }, + }, + default: '', + description: 'Value uniquely identifying the build.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'build', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'string', + default: '', + placeholder: 'build.commit', + description: 'List of attributes to eager load.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* build:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'build', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'build', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'build', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'string', + default: '', + placeholder: 'build.commit', + description: 'List of attributes to eager load.', + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + }, + { + name: 'DESC', + value: 'desc', + } + ], + default: 'asc', + description: 'You may specify order to sort your response.', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'ID', + value: 'id', + }, + { + name: 'Created At', + value: 'created_at', + }, + { + name: 'Started At', + value: 'started_at', + }, + { + name: 'Finished At', + value: 'finished_at', + }, + { + name: 'Finished At', + value: 'finished_at', + }, + { + name: 'Number', + value: 'number', + }, + ], + default: 'number', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* build:restart */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Build ID', + name: 'buildId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'restart', + ], + resource: [ + 'build', + ], + }, + }, + default: '', + description: 'Value uniquely identifying the build.', + }, +/* -------------------------------------------------------------------------- */ +/* build:trigger */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Slug', + name: 'slug', + type: 'string', + displayOptions: { + show: { + operation: [ + 'trigger', + ], + resource: [ + 'build', + ], + }, + }, + default: '', + description: 'Same as {ownerName}/{repositoryName}', + }, + { + displayName: 'Branch', + name: 'branch', + type: 'string', + displayOptions: { + show: { + operation: [ + 'trigger', + ], + resource: [ + 'build', + ], + }, + }, + default: '', + placeholder: 'master', + description: 'Branch requested to be built.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'build', + ], + operation: [ + 'trigger', + ], + }, + }, + options: [ + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: 'Travis-ci status message attached to the request.', + }, + { + displayName: 'Merge Mode', + name: 'mergeMode', + type: 'options', + options: [ + { + name: 'Deep Merge Append', + value: 'deep_merge_append', + }, + { + name: 'Deep Merge Prepend', + value: 'deep_merge_prepend', + }, + { + name: 'Deep Merge', + value: 'deep_merge', + }, + { + name: 'Merge', + value: 'merge', + }, + { + name: 'Replace', + value: 'replace', + }, + ], + default: '', + description: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/TravisCi/GenericFunctions.ts b/packages/nodes-base/nodes/TravisCi/GenericFunctions.ts new file mode 100644 index 0000000000..4e93ad159f --- /dev/null +++ b/packages/nodes-base/nodes/TravisCi/GenericFunctions.ts @@ -0,0 +1,78 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import { + get, + } from 'lodash'; + + import * as querystring from 'querystring'; + +export async function travisciApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('travisCiApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + 'Travis-API-Version': '3', + 'Accept': 'application/json', + 'Content-Type': 'application.json', + 'Authorization': `token ${credentials.apiToken}`, + }, + method, + qs, + body, + uri: uri ||`https://api.travis-ci.com${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (err) { + if (err.response && err.response.body && err.response.body.error_message) { + // Try to return the error prettier + throw new Error(`TravisCI error response [${err.statusCode}]: ${err.response.body.error_message}`); + } + + // If that data does not exist for some reason return the actual error + throw err; + } +} + +/** + * Make an API request to paginated TravisCI endpoint + * and return all results + */ +export async function travisciApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + do { + responseData = await travisciApiRequest.call(this, method, resource, body, query); + const path = get(responseData, '@pagination.next.@href'); + if (path !== undefined) { + query = querystring.parse(path); + } + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['@pagination']['is_last'] !== true + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/TravisCi/TravisCi.node.ts b/packages/nodes-base/nodes/TravisCi/TravisCi.node.ts new file mode 100644 index 0000000000..3efabf165b --- /dev/null +++ b/packages/nodes-base/nodes/TravisCi/TravisCi.node.ts @@ -0,0 +1,151 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; + +import { + buildFields, + buildOperations, +} from './BuildDescription'; + +import { + travisciApiRequest, + travisciApiRequestAllItems, +} from './GenericFunctions'; + +export class TravisCi implements INodeType { + description: INodeTypeDescription = { + displayName: 'TravisCI', + name: 'travisCi', + icon: 'file:travisCi.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume TravisCI API', + defaults: { + name: 'TravisCI', + color: '#FF0000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'travisCiApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: ' Build', + value: 'build', + }, + ], + default: 'build', + description: 'Resource to consume.', + }, + ...buildOperations, + ...buildFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + 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 === 'build') { + //https://developer.travis-ci.com/resource/build#find + if (operation === 'get') { + const buildId = this.getNodeParameter('buildId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.include) { + qs.include = additionalFields.include as string; + } + + responseData = await travisciApiRequest.call(this, 'GET', `/build/${buildId}`, {}, qs); + } + //https://developer.travis-ci.com/resource/builds#for_current_user + if (operation === 'getAll') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (additionalFields.sortBy) { + qs.sort_by = additionalFields.sortBy; + } + + if (additionalFields.sortBy && additionalFields.order) { + qs.sort_by = `${additionalFields.sortBy}:${additionalFields.order}`; + } + + if (additionalFields.include) { + qs.include = additionalFields.include; + } + + if (returnAll === true) { + responseData = await travisciApiRequestAllItems.call(this, 'builds', 'GET', '/builds', {}, qs); + + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await travisciApiRequest.call(this, 'GET', '/builds', {}, qs); + responseData = responseData.builds; + } + } + //https://developer.travis-ci.com/resource/build#cancel + if (operation === 'cancel') { + const buildId = this.getNodeParameter('buildId', i) as string; + responseData = await travisciApiRequest.call(this, 'POST', `/build/${buildId}/cancel`, {}, qs); + } + //https://developer.travis-ci.com/resource/build#restart + if (operation === 'restart') { + const buildId = this.getNodeParameter('buildId', i) as string; + responseData = await travisciApiRequest.call(this, 'POST', `/build/${buildId}/restart`, {}, qs); + } + //https://developer.travis-ci.com/resource/requests#create + if (operation === 'trigger') { + let slug = this.getNodeParameter('slug', i) as string; + const branch = this.getNodeParameter('branch', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + slug = slug.replace(new RegExp(/\//g), '%2F'); + + const request: IDataObject = { + branch, + }; + + if (additionalFields.message) { + request.message = additionalFields.message as string; + } + + if (additionalFields.mergeMode) { + request.merge_mode = additionalFields.mergeMode as string; + } + + responseData = await travisciApiRequest.call(this, 'POST', `/repo/${slug}/requests`, JSON.stringify({request})); + } + } + 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/nodes/TravisCi/travisCi.png b/packages/nodes-base/nodes/TravisCi/travisCi.png new file mode 100644 index 0000000000..d655d44398 Binary files /dev/null and b/packages/nodes-base/nodes/TravisCi/travisCi.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6f22c97458..b2fd3c01c6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -142,6 +142,7 @@ "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", + "dist/credentials/TravisCiApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js", "dist/credentials/TwitterOAuth1Api.credentials.js", @@ -311,6 +312,7 @@ "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js", "dist/nodes/Toggl/TogglTrigger.node.js", + "dist/nodes/TravisCi/TravisCi.node.js", "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js",