diff --git a/packages/nodes-base/credentials/BitlyApi.credentials.ts b/packages/nodes-base/credentials/BitlyApi.credentials.ts new file mode 100644 index 0000000000..a6c5259877 --- /dev/null +++ b/packages/nodes-base/credentials/BitlyApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class BitlyApi implements ICredentialType { + name = 'bitlyApi'; + displayName = 'Bitly API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Bitly/Bitly.node.ts b/packages/nodes-base/nodes/Bitly/Bitly.node.ts new file mode 100644 index 0000000000..700172ed0f --- /dev/null +++ b/packages/nodes-base/nodes/Bitly/Bitly.node.ts @@ -0,0 +1,188 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + linkFields, + linkOperations +} from './LinkDescription'; +import { + bitlyApiRequest, + bitlyApiRequestAllItems, +} from './GenericFunctions'; + +export class Bitly implements INodeType { + description: INodeTypeDescription = { + displayName: 'Bitly', + name: 'bitly', + icon: 'file:bitly.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Bitly API', + defaults: { + name: 'Bitly', + color: '#d3643b', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'bitlyApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: ' Link', + value: 'link', + }, + ], + default: 'link', + description: 'Resource to consume.', + }, + ...linkOperations, + ...linkFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await bitlyApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.guid; + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + }, + // Get all the available tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const groupId = this.getCurrentNodeParameter('group') as string; + const returnData: INodePropertyOptions[] = []; + const tags = await bitlyApiRequestAllItems.call(this, 'tags', 'GET', `groups/${groupId}/tags`); + for (const tag of tags) { + const tagName = tag; + const tagId = tag; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + }, + }; + + 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 === 'link') { + if (operation === 'create') { + const longUrl = this.getNodeParameter('longUrl', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + long_url: longUrl, + }; + if (additionalFields.title) { + body.title = additionalFields.title as string; + } + if (additionalFields.domain) { + body.domain = additionalFields.domain as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + const deeplinks = (this.getNodeParameter('deeplink', i) as IDataObject).deeplinkUi as IDataObject[]; + if (deeplinks) { + for (const deeplink of deeplinks) { + //@ts-ignore + body.deeplinks.push({ + app_uri_path: deeplink.appUriPath, + install_type: deeplink.installType, + install_url: deeplink.installUrl, + app_id: deeplink.appId, + }) + } + } + responseData = await bitlyApiRequest.call(this, 'POST', '/bitlinks', body); + } + if (operation === 'update') { + const linkId = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = {}; + if (updateFields.longUrl) { + body.long_url = updateFields.longUrl as string; + } + if (updateFields.title) { + body.title = updateFields.title as string; + } + if (updateFields.archived !== undefined) { + body.archived = updateFields.archived as boolean; + } + if (updateFields.domain) { + body.domain = updateFields.domain as string; + } + if (updateFields.group) { + body.group = updateFields.group as string; + } + if (updateFields.tags) { + body.tags = updateFields.tags as string[]; + } + const deeplinks = (this.getNodeParameter('deeplink', i) as IDataObject).deeplinkUi as IDataObject[]; + if (deeplinks) { + for (const deeplink of deeplinks) { + //@ts-ignore + body.deeplinks.push({ + app_uri_path: deeplink.appUriPath, + install_type: deeplink.installType, + install_url: deeplink.installUrl, + app_id: deeplink.appId, + }) + } + } + responseData = await bitlyApiRequest.call(this, 'PATCH', `/bitlinks/${linkId}`, body); + } + if (operation === 'get') { + const linkId = this.getNodeParameter('id', i) as string; + responseData = await bitlyApiRequest.call(this, 'GET', `/bitlinks/${linkId}`); + } + } + 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/Bitly/GenericFunctions.ts b/packages/nodes-base/nodes/Bitly/GenericFunctions.ts new file mode 100644 index 0000000000..5049b08044 --- /dev/null +++ b/packages/nodes-base/nodes/Bitly/GenericFunctions.ts @@ -0,0 +1,57 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function bitlyApiRequest(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('bitlyApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { Authorization: `Bearer ${credentials.accessToken}`}, + method, + qs, + body, + uri: uri ||`https://api-ssl.bitly.com/v4${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(err); + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function bitlyApiRequestAllItems(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; + query.size = 50; + + do { + responseData = await bitlyApiRequest.call(this, method, resource, body, query, uri); + returnData.push.apply(returnData, responseData[propertyName]); + if (responseData.pagination && responseData.pagination.next) { + uri = responseData.pagination.next; + }; + } while ( + responseData.pagination !== undefined && + responseData.pagination.next !== undefined + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Bitly/LinkDescription.ts b/packages/nodes-base/nodes/Bitly/LinkDescription.ts new file mode 100644 index 0000000000..5cd872154a --- /dev/null +++ b/packages/nodes-base/nodes/Bitly/LinkDescription.ts @@ -0,0 +1,319 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const linkOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'link', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a link', + }, + { + name: 'Update', + value: 'update', + description: 'Update a link', + }, + { + name: 'Get', + value: 'get', + description: 'Get a link', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const linkFields = [ + +/* -------------------------------------------------------------------------- */ +/* link:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Long URL', + name: 'longUrl', + type: 'string', + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: 'bit.ly', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + default: [], + typeOptions: { + loadOptionsMethod: 'getTags', + loadOptionsDependsOn: [ + 'group', + ] + }, + }, + ], + }, + { + displayName: 'Deeplinks', + name: 'deeplink', + placeholder: 'Add Deep Link', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + name: 'deeplinkUi', + displayName: 'Deep Link', + values: [ + { + displayName: 'App URI Path', + name: 'appUriPath', + type: 'string', + default: '', + }, + { + displayName: 'Install Type', + name: 'installType', + type: 'string', + default: '', + }, + { + displayName: 'Install URL', + name: 'installUrl', + type: 'string', + default: '', + }, + { + displayName: 'App ID', + name: 'appId', + type: 'string', + default: '', + }, + ] + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* link:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Bitlink', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Long URL', + name: 'longUrl', + type: 'string', + default: '', + }, + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: false, + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: 'bit.ly', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + default: [], + typeOptions: { + loadOptionsMethod: 'getTags', + loadOptionsDependsOn: [ + 'group', + ] + }, + }, + ], + }, + { + displayName: 'Deeplinks', + name: 'deeplink', + placeholder: 'Add Deep Link', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + options: [ + { + name: 'deeplinkUi', + displayName: 'Deep Link', + values: [ + { + displayName: 'App URI Path', + name: 'appUriPath', + type: 'string', + default: '', + }, + { + displayName: 'Install Type', + name: 'installType', + type: 'string', + default: '', + }, + { + displayName: 'Install URL', + name: 'installUrl', + type: 'string', + default: '', + }, + { + displayName: 'App ID', + name: 'appId', + type: 'string', + default: '', + }, + ] + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* link:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Bitlink', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'link', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Bitly/bitly.png b/packages/nodes-base/nodes/Bitly/bitly.png new file mode 100644 index 0000000000..5d000ffde4 Binary files /dev/null and b/packages/nodes-base/nodes/Bitly/bitly.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index f7d13755c6..22609e4af1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -33,6 +33,7 @@ "dist/credentials/AsanaApi.credentials.js", "dist/credentials/Aws.credentials.js", "dist/credentials/BitbucketApi.credentials.js", + "dist/credentials/BitlyApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", "dist/credentials/CodaApi.credentials.js", @@ -100,6 +101,7 @@ "dist/nodes/Aws/AwsLambda.node.js", "dist/nodes/Aws/AwsSns.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", + "dist/nodes/Bitly/Bitly.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/ClickUp/ClickUp.node.js",