diff --git a/packages/nodes-base/credentials/BitbucketApi.credentials.ts b/packages/nodes-base/credentials/BitbucketApi.credentials.ts new file mode 100644 index 0000000000..5a12902fe3 --- /dev/null +++ b/packages/nodes-base/credentials/BitbucketApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class BitbucketApi implements ICredentialType { + name = 'bitbucketApi'; + displayName = 'Bitbucket API'; + properties = [ + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'App Password', + name: 'appPassword', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts b/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts new file mode 100644 index 0000000000..1e506deb92 --- /dev/null +++ b/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts @@ -0,0 +1,358 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, + INodePropertyOptions, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + bitbucketApiRequest, + bitbucketApiRequestAllItems, +} from './GenericFunctions'; + +export class BitbucketTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Bitbucket Trigger', + name: 'bitbucket', + icon: 'file:bitbucket.png', + group: ['trigger'], + version: 1, + description: 'Handle Bitbucket events via webhooks', + defaults: { + name: 'Bitbucket Trigger', + color: '#0052cc', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'bitbucketApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + required: true, + options: [ + { + name: 'User', + value: 'user', + }, + { + name: 'Team', + value: 'team', + }, + { + name: 'Repository', + value: 'repository', + }, + ], + default: 'user', + description: 'The resource to operate on.', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + displayOptions: { + show: { + resource: [ + 'user' + ] + } + }, + typeOptions: { + loadOptionsMethod: 'getUsersEvents', + }, + options: [], + required: true, + default: [], + description: 'The events to listen to.', + }, + { + displayName: 'Team', + name: 'team', + type: 'options', + displayOptions: { + show: { + resource: [ + 'team' + ] + } + }, + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + required: true, + default: '', + description: 'The team of which to listen to the events.', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + displayOptions: { + show: { + resource: [ + 'team' + ] + } + }, + typeOptions: { + loadOptionsMethod: 'getTeamEvents', + }, + options: [], + required: true, + default: [], + description: 'The events to listen to.', + }, + { + displayName: 'Repository', + name: 'repository', + type: 'options', + displayOptions: { + show: { + resource: [ + 'repository' + ] + } + }, + typeOptions: { + loadOptionsMethod: 'getRepositories', + }, + required: true, + default: '', + description: 'The repository of which to listen to the events.', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + displayOptions: { + show: { + resource: [ + 'repository' + ] + } + }, + typeOptions: { + loadOptionsMethod: 'getRepositoriesEvents', + }, + options: [], + required: true, + default: [], + description: 'The events to listen to.', + }, + ], + + }; + + methods = { + loadOptions: { + // Get all the events to display them to user so that he can + // select them easily + async getUsersEvents(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/user'); + for (const event of events) { + const eventName = event.event; + const eventId = event.event; + const eventDescription = event.description; + returnData.push({ + name: eventName, + value: eventId, + description: eventDescription, + }); + } + return returnData; + }, + // Get all the events to display them to user so that he can + // select them easily + async getTeamEvents(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/team'); + for (const event of events) { + const eventName = event.event; + const eventId = event.event; + const eventDescription = event.description; + returnData.push({ + name: eventName, + value: eventId, + description: eventDescription, + }); + } + return returnData; + }, + // Get all the events to display them to user so that he can + // select them easily + async getRepositoriesEvents(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/repository'); + for (const event of events) { + const eventName = event.event; + const eventId = event.event; + const eventDescription = event.description; + returnData.push({ + name: eventName, + value: eventId, + description: eventDescription, + }); + } + return returnData; + }, + // Get all the repositories to display them to user so that he can + // select them easily + async getRepositories(this: ILoadOptionsFunctions): Promise { + const credentials = this.getCredentials('bitbucketApi'); + const returnData: INodePropertyOptions[] = []; + const repositories = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', `/repositories/${credentials!.username}`); + for (const repository of repositories) { + const repositoryName = repository.slug; + const repositoryId = repository.slug; + const repositoryDescription = repository.description; + returnData.push({ + name: repositoryName, + value: repositoryId, + description: repositoryDescription, + }); + } + return returnData; + }, + // Get all the teams to display them to user so that he can + // select them easily + async getTeams(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = { + role: 'member', + }; + const teams = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/teams', {}, qs); + for (const team of teams) { + const teamName = team.display_name; + const teamId = team.username; + returnData.push({ + name: teamName, + value: teamId, + }); + } + return returnData; + }, + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + let endpoint = ''; + const credentials = this.getCredentials('bitbucketApi'); + const resource = this.getNodeParameter('resource', 0) as string; + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + if (resource === 'user') { + endpoint = `/users/${credentials!.username}/hooks/${webhookData.webhookId}`; + } + if (resource === 'team') { + const team = this.getNodeParameter('team', 0) as string; + endpoint = `/teams/${team}/hooks/${webhookData.webhookId}`; + } + if (resource === 'repository') { + const repository = this.getNodeParameter('repository', 0) as string; + endpoint = `/repositories/${credentials!.username}/${repository}/hooks/${webhookData.webhookId}`; + } + try { + await bitbucketApiRequest.call(this, 'GET', endpoint); + } catch (e) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + let responseData; + let endpoint = ''; + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string[]; + const resource = this.getNodeParameter('resource', 0) as string; + const credentials = this.getCredentials('bitbucketApi'); + + if (resource === 'user') { + endpoint = `/users/${credentials!.username}/hooks`; + } + if (resource === 'team') { + const team = this.getNodeParameter('team', 0) as string; + endpoint = `/teams/${team}/hooks`; + } + if (resource === 'repository') { + const repository = this.getNodeParameter('repository', 0) as string; + endpoint = `/repositories/${credentials!.username}/${repository}/hooks`; + } + const body: IDataObject = { + description: 'n8n webhook', + url: webhookUrl, + active: true, + events, + }; + responseData = await bitbucketApiRequest.call(this, 'POST', endpoint, body); + webhookData.webhookId = responseData.uuid.replace('{', '').replace('}', ''); + return true; + }, + async delete(this: IHookFunctions): Promise { + let endpoint = ''; + const webhookData = this.getWorkflowStaticData('node'); + const credentials = this.getCredentials('bitbucketApi'); + const resource = this.getNodeParameter('resource', 0) as string; + if (resource === 'user') { + endpoint = `/users/${credentials!.username}/hooks/${webhookData.webhookId}`; + } + if (resource === 'team') { + const team = this.getNodeParameter('team', 0) as string; + endpoint = `/teams/${team}/hooks/${webhookData.webhookId}`; + } + if (resource === 'repository') { + const repository = this.getNodeParameter('repository', 0) as string; + endpoint = `/repositories/${credentials!.username}/${repository}/hooks/${webhookData.webhookId}`; + } + try { + await bitbucketApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const headerData = this.getHeaderData() as IDataObject; + const webhookData = this.getWorkflowStaticData('node'); + if (headerData['x-hook-uuid'] !== webhookData.webhookId) { + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Bitbucket/GenericFunctions.ts b/packages/nodes-base/nodes/Bitbucket/GenericFunctions.ts new file mode 100644 index 0000000000..5f9c784464 --- /dev/null +++ b/packages/nodes-base/nodes/Bitbucket/GenericFunctions.ts @@ -0,0 +1,59 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function bitbucketApiRequest(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('bitbucketApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + method, + auth: { + user: credentials.username as string, + password: credentials.appPassword as string, + }, + qs, + body, + uri: uri ||`https://api.bitbucket.org/2.0${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) { + throw new Error('Bitbucket Error: ' + err.message); + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function bitbucketApiRequestAllItems(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; + + let uri: string | undefined; + + do { + responseData = await bitbucketApiRequest.call(this, method, resource, body, query, uri); + uri = responseData.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.next !== undefined + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Bitbucket/bitbucket.png b/packages/nodes-base/nodes/Bitbucket/bitbucket.png new file mode 100644 index 0000000000..e064031b9e Binary files /dev/null and b/packages/nodes-base/nodes/Bitbucket/bitbucket.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b18f08656b..b92a938354 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -31,6 +31,7 @@ "dist/credentials/Amqp.credentials.js", "dist/credentials/AsanaApi.credentials.js", "dist/credentials/Aws.credentials.js", + "dist/credentials/BitbucketApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", @@ -72,9 +73,9 @@ "dist/credentials/TwilioApi.credentials.js", "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", - "dist/credentials/TodoistApi.credentials.js", - "dist/credentials/TypeformApi.credentials.js", - "dist/credentials/TogglApi.credentials.js", + "dist/credentials/TodoistApi.credentials.js", + "dist/credentials/TypeformApi.credentials.js", + "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", "dist/credentials/WordpressApi.credentials.js" ], @@ -87,6 +88,7 @@ "dist/nodes/Asana/AsanaTrigger.node.js", "dist/nodes/Aws/AwsLambda.node.js", "dist/nodes/Aws/AwsSns.node.js", + "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/Cron.node.js",