From 3dfa55d850af4fc010c5e989efb15e29e9189e59 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 4 Aug 2020 09:48:05 -0400 Subject: [PATCH] :sparkles: TravisCI-Node (#756) * :sparkles: TravisCI-Node * :zap: Small improvement --- .../credentials/TravisCiApi.credentials.ts | 17 + .../nodes/TravisCi/BuildDescription.ts | 351 ++++++++++++++++++ .../nodes/TravisCi/GenericFunctions.ts | 78 ++++ .../nodes/TravisCi/TravisCi.node.ts | 151 ++++++++ .../nodes-base/nodes/TravisCi/travisCi.png | Bin 0 -> 7056 bytes packages/nodes-base/package.json | 2 + 6 files changed, 599 insertions(+) create mode 100644 packages/nodes-base/credentials/TravisCiApi.credentials.ts create mode 100644 packages/nodes-base/nodes/TravisCi/BuildDescription.ts create mode 100644 packages/nodes-base/nodes/TravisCi/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/TravisCi/TravisCi.node.ts create mode 100644 packages/nodes-base/nodes/TravisCi/travisCi.png 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 0000000000000000000000000000000000000000..d655d4439899f9fa948d9d35e94c7baa76d96c73 GIT binary patch literal 7056 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