From 4de2319a5a47ad5bacdd0ccb4e88043b458000d1 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 12 Jul 2019 17:54:15 +0200 Subject: [PATCH] :sparkles: Add Trello-Nodes --- .../credentials/TrelloApi.credentials.ts | 30 + .../nodes/Trello/GenericFunctions.ts | 51 + .../nodes-base/nodes/Trello/Trello.node.ts | 1434 +++++++++++++++++ .../nodes/Trello/TrelloTrigger.node.ts | 220 +++ packages/nodes-base/nodes/Trello/trello.png | Bin 0 -> 962 bytes packages/nodes-base/package.json | 3 + 6 files changed, 1738 insertions(+) create mode 100644 packages/nodes-base/credentials/TrelloApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Trello/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Trello/Trello.node.ts create mode 100644 packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Trello/trello.png diff --git a/packages/nodes-base/credentials/TrelloApi.credentials.ts b/packages/nodes-base/credentials/TrelloApi.credentials.ts new file mode 100644 index 0000000000..fe33e13893 --- /dev/null +++ b/packages/nodes-base/credentials/TrelloApi.credentials.ts @@ -0,0 +1,30 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class TrelloApi implements ICredentialType { + name = 'trelloApi'; + displayName = 'Trello API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'OAuth Secret', + name: 'oauthSecret', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Trello/GenericFunctions.ts b/packages/nodes-base/nodes/Trello/GenericFunctions.ts new file mode 100644 index 0000000000..831d3f3b43 --- /dev/null +++ b/packages/nodes-base/nodes/Trello/GenericFunctions.ts @@ -0,0 +1,51 @@ +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { OptionsWithUri } from 'request'; +import { IDataObject } from 'n8n-workflow'; + + +/** + * Make an API request to Trello + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('trelloApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + query = query || {}; + + query.key = credentials.apiKey; + query.token = credentials.apiToken; + + const options: OptionsWithUri = { + headers: { + }, + method, + body, + qs: query, + uri: `https://api.trello.com/1/${endpoint}`, + json: true, + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.statusCode === 401) { + throw new Error('The Trello credentials are not valid!'); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Trello/Trello.node.ts b/packages/nodes-base/nodes/Trello/Trello.node.ts new file mode 100644 index 0000000000..89b8079323 --- /dev/null +++ b/packages/nodes-base/nodes/Trello/Trello.node.ts @@ -0,0 +1,1434 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; + +import { + apiRequest, +} from './GenericFunctions'; + +export class Trello implements INodeType { + description: INodeTypeDescription = { + displayName: 'Trello', + name: 'trello', + icon: 'file:trello.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Create, change and delete boards and cards', + defaults: { + name: 'Trello', + color: '#026aa7', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'trelloApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Board', + value: 'board', + }, + { + name: 'Card', + value: 'card', + }, + { + name: 'List', + value: 'list', + }, + ], + default: 'card', + description: 'The resource to operate on.', + }, + + + + // ---------------------------------- + // board + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'board', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new board', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a board', + }, + { + name: 'Get', + value: 'get', + description: 'Get the data of a board', + }, + { + name: 'Update', + value: 'update', + description: 'Update a board', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // board:create + // ---------------------------------- + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'My board', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'board', + ], + }, + }, + description: 'The name of the board', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'board', + ], + }, + }, + description: 'The description of the board', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'board', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Aging', + name: 'prefs_cardAging', + type: 'options', + options: [ + { + name: 'Pirate', + value: 'pirate', + }, + { + name: 'Regular', + value: 'regular', + }, + ], + default: 'regular', + description: 'Determines the type of card aging that should take place on the board if card aging is enabled.', + }, + { + displayName: 'Background', + name: 'prefs_background', + type: 'string', + default: 'blue', + description: 'The id of a custom background or one of: blue, orange, green, red, purple, pink, lime, sky, grey.', + }, + { + displayName: 'Comments', + name: 'prefs_comments', + type: 'options', + options: [ + { + name: 'Disabled', + value: 'disabled', + }, + { + name: 'Members', + value: 'members', + }, + { + name: 'Observers', + value: 'observers', + }, + { + name: 'Organization', + value: 'org', + }, + { + name: 'Public', + value: 'public', + }, + ], + default: 'members', + description: 'Who can comment on cards on this board.', + }, + { + displayName: 'Covers', + name: 'prefs_cardCovers', + type: 'boolean', + default: true, + description: 'Determines whether card covers are enabled.', + }, + { + displayName: 'Invitations', + name: 'prefs_invitations', + type: 'options', + options: [ + { + name: 'Admins', + value: 'admins', + }, + { + name: 'Members', + value: 'members', + }, + ], + default: 'members', + description: 'Determines what types of members can invite users to join.', + }, + { + displayName: 'Keep From Source', + name: 'keepFromSource', + type: 'string', + default: 'none', + description: 'To keep cards from the original board pass in the value cards.', + }, + { + displayName: 'Labels', + name: 'defaultLabels', + type: 'boolean', + default: true, + description: 'Determines whether to use the default set of labels.', + }, + { + displayName: 'Lists', + name: 'defaultLists', + type: 'boolean', + default: true, + description: 'Determines whether to add the default set of lists to a board(To Do, Doing, Done).It is ignored if idBoardSource is provided.', + }, + { + displayName: 'Organization ID', + name: 'idOrganization', + type: 'string', + default: '', + description: 'The id or name of the team the board should belong to.', + }, + { + displayName: 'Permission Level', + name: 'prefs_permissionLevel', + type: 'options', + options: [ + { + name: 'Organization', + value: 'org', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Public', + value: 'public', + }, + ], + default: 'private', + description: 'The permissions level of the board.', + }, + { + displayName: 'Power Ups', + name: 'powerUps', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + }, + { + name: 'Calendar', + value: 'calendar', + }, + { + name: 'Card Aging', + value: 'cardAging', + }, + { + name: 'Recap', + value: 'recap', + }, + { + name: 'Voting', + value: 'voting', + }, + ], + default: 'all', + description: 'The Power-Ups that should be enabled on the new board.', + }, + { + displayName: 'Self Join', + name: 'prefs_selfJoin', + type: 'boolean', + default: true, + description: 'Determines whether users can join the boards themselves or whether they have to be invited.', + }, + { + displayName: 'Source IDs', + name: 'idBoardSource', + type: 'string', + default: '', + description: 'The id of a board to copy into the new board.', + }, + { + displayName: 'Voting', + name: 'prefs_voting', + type: 'options', + options: [ + { + name: 'Disabled', + value: 'disabled', + }, + { + name: 'Members', + value: 'members', + }, + { + name: 'Observers', + value: 'observers', + }, + { + name: 'Organization', + value: 'org', + }, + { + name: 'Public', + value: 'public', + }, + ], + default: 'disabled', + description: 'Who can vote on this board.', + }, + ], + }, + + // ---------------------------------- + // board:delete + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'board', + ], + }, + }, + description: 'The ID of the board to delete.', + }, + + // ---------------------------------- + // board:get + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'board', + ], + }, + }, + description: 'The ID of the board to get.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'board', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list:
closed, dateLastActivity, dateLastView, desc, descData,
idOrganization, invitations, invited, labelNames, memberships,
name, pinned, powerUps, prefs, shortLink, shortUrl,
starred, subscribed, url', + }, + { + displayName: 'Plugin Data', + name: 'pluginData', + type: 'boolean', + default: false, + description: 'Whether to include pluginData on the card with the response.', + }, + ], + }, + + // ---------------------------------- + // board:update + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'board', + ], + }, + }, + description: 'The ID of the board to update.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'board', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Closed', + name: 'closed', + type: 'boolean', + default: false, + description: 'Whether the board is closed.', + }, + { + displayName: 'Description', + name: 'desc', + type: 'string', + default: '', + description: 'New description of the board', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'New name of the board', + }, + { + displayName: 'Organization ID', + name: 'idOrganization', + type: 'string', + default: '', + description: 'The id of the team the board should be moved to.', + }, + { + displayName: 'Subscribed', + name: 'subscribed', + type: 'boolean', + default: false, + description: 'Whether the acting user is subscribed to the board.', + }, + ], + }, + + + + // ---------------------------------- + // card + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'card', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new card', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a card', + }, + { + name: 'Get', + value: 'get', + description: 'Get the data of a card', + }, + { + name: 'Update', + value: 'update', + description: 'Update a card', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // card:create + // ---------------------------------- + { + displayName: 'List ID', + name: 'listId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'card', + ], + }, + }, + description: 'The id of the list to create card in', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'My card', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'card', + ], + }, + }, + description: 'The name of the card', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'card', + ], + }, + }, + description: 'The description of the card', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'card', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Due Date', + name: 'due', + type: 'dateTime', + default: '', + description: 'A due date for the card.', + }, + { + displayName: 'Due Complete', + name: 'dueComplete', + type: 'boolean', + default: false, + description: 'If the card is completed.', + }, + { + displayName: 'Position', + name: 'pos', + type: 'string', + default: 'bottom', + description: 'The position of the new card. top, bottom, or a positive float.', + }, + { + displayName: 'Member IDs', + name: 'idMembers', + type: 'string', + default: '', + description: 'Comma-separated list of member IDs to add to the card.', + }, + { + displayName: 'Label IDs', + name: 'idLabels', + type: 'string', + default: '', + description: 'Comma-separated list of label IDs to add to the card.', + }, + { + displayName: 'URL Source', + name: 'urlSource', + type: 'string', + default: '', + description: 'A source URL to attach to card.', + }, + { + displayName: 'Source ID', + name: 'idCardSource', + type: 'string', + default: '', + description: 'The ID of a card to copy into the new card.', + }, + { + displayName: 'Keep from source', + name: 'keepFromSource', + type: 'string', + default: 'all', + description: 'If using idCardSource you can specify which properties to copy over. all or comma-separated list of: attachments, checklists, comments, due, labels, members, stickers', + }, + ], + }, + + // ---------------------------------- + // card:delete + // ---------------------------------- + { + displayName: 'Card ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'card', + ], + }, + }, + description: 'The ID of the card to delete.', + }, + + // ---------------------------------- + // card:get + // ---------------------------------- + { + displayName: 'Card ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'card', + ], + }, + }, + description: 'The ID of the card to get.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'card', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list:
badges, checkItemStates, closed, dateLastActivity, desc,
descData, due, email, idBoard, idChecklists, idLabels, idList,
idMembers, idShort, idAttachmentCover, manualCoverAttachment
, labels, name, pos, shortUrl, url', + }, + { + displayName: 'Board', + name: 'board', + type: 'boolean', + default: false, + description: 'Whether to return the board object the card is on.', + }, + { + displayName: 'Board Fields', + name: 'board_fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list:
name, desc, descData, closed, idOrganization, pinned, url, prefs', + }, + { + displayName: 'Custom Field Items', + name: 'customFieldItems', + type: 'boolean', + default: false, + description: 'Whether to include the customFieldItems.', + }, + { + displayName: 'Members', + name: 'members', + type: 'boolean', + default: false, + description: 'Whether to return member objects for members on the card.', + }, + { + displayName: 'Member Fields', + name: 'member_fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list:
avatarHash, fullName, initials, username', + }, + { + displayName: 'Plugin Data', + name: 'pluginData', + type: 'boolean', + default: false, + description: 'Whether to include pluginData on the card with the response.', + }, + { + displayName: 'Stickers', + name: 'stickers', + type: 'boolean', + default: false, + description: 'Whether to include sticker models with the response.', + }, + { + displayName: 'Sticker Fields', + name: 'sticker_fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list of sticker fields.', + }, + ], + }, + + // ---------------------------------- + // card:update + // ---------------------------------- + { + displayName: 'Card ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'card', + ], + }, + }, + description: 'The ID of the card to update.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'card', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Attachment Cover', + name: 'idAttachmentCover', + type: 'string', + default: '', + description: 'The ID of the image attachment the card should use as its cover, or null for none.', + }, + { + displayName: 'Board ID', + name: 'idBoard', + type: 'string', + default: '', + description: 'The ID of the board the card should be on.', + }, + { + displayName: 'Closed', + name: 'closed', + type: 'boolean', + default: false, + description: 'Whether the board is closed.', + }, + { + displayName: 'Description', + name: 'desc', + type: 'string', + default: '', + description: 'New description of the board.', + }, + { + displayName: 'Due Date', + name: 'due', + type: 'dateTime', + default: '', + description: 'A due date for the card.', + }, + { + displayName: 'Due Complete', + name: 'dueComplete', + type: 'boolean', + default: false, + description: 'If the card is completed.', + }, + { + displayName: 'Label IDs', + name: 'idLabels', + type: 'string', + default: '', + description: 'Comma-separated list of label IDs to set on card.', + }, + { + displayName: 'List ID', + name: 'idList', + type: 'string', + default: '', + description: 'The ID of the list the card should be in.', + }, + { + displayName: 'Member IDs', + name: 'idMembers', + type: 'string', + default: '', + description: 'Comma-separated list of member IDs to set on card.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'New name of the board', + }, + { + displayName: 'Position', + name: 'pos', + type: 'string', + default: 'bottom', + description: 'The position of the card. top, bottom, or a positive float.', + }, + { + displayName: 'Subscribed', + name: 'subscribed', + type: 'boolean', + default: false, + description: 'Whether the acting user is subscribed to the board.', + }, + ], + }, + + + + // ---------------------------------- + // list + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'list', + ], + }, + }, + options: [ + { + name: 'Archive', + value: 'archive', + description: 'Archive/Unarchive a list', + }, + { + name: 'Create', + value: 'create', + description: 'Create a new list', + }, + { + name: 'Get', + value: 'get', + description: 'Get the data of a list', + }, + { + name: 'Update', + value: 'update', + description: 'Update a list', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // list:archive + // ---------------------------------- + { + displayName: 'List ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'archive', + ], + resource: [ + 'list', + ], + }, + }, + description: 'The ID of the list to archive or unarchive.', + }, + { + displayName: 'Archive', + name: 'archive', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'archive', + ], + resource: [ + 'list', + ], + }, + }, + description: 'If the list should be archived or unarchived.', + }, + + // ---------------------------------- + // list:create + // ---------------------------------- + { + displayName: 'Board ID', + name: 'idBoard', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, + }, + description: 'The ID of the board the list should be created in', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'My list', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, + }, + description: 'The name of the list', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'List Source', + name: 'idListSource', + type: 'string', + default: '', + description: 'ID of the list to copy into the new list.', + }, + { + displayName: 'Position', + name: 'pos', + type: 'string', + default: 'bottom', + description: 'The position of the new list. top, bottom, or a positive float.', + }, + ], + }, + + // ---------------------------------- + // list:get + // ---------------------------------- + { + displayName: 'List ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'list', + ], + }, + }, + description: 'The ID of the list to get.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'list', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list of fields.', + }, + ], + }, + + // ---------------------------------- + // list:update + // ---------------------------------- + { + displayName: 'List ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'list', + ], + }, + }, + description: 'The ID of the list to update.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'list', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Board ID', + name: 'idBoard', + type: 'string', + default: '', + description: 'ID of a board the list should be moved to.', + }, + { + displayName: 'Closed', + name: 'closed', + type: 'boolean', + default: false, + description: 'Whether the list is closed.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'New name of the list', + }, + { + displayName: 'Position', + name: 'pos', + type: 'string', + default: 'bottom', + description: 'The position of the list. top, bottom, or a positive float.', + }, + { + displayName: 'Subscribed', + name: 'subscribed', + type: 'boolean', + default: false, + description: 'Whether the acting user is subscribed to the list.', + }, + ], + }, + + ], + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + + // For Post + let body: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod: string; + let endpoint: string; + + for (let i = 0; i < items.length; i++) { + requestMethod = 'GET'; + endpoint = ''; + body = {}; + qs = {}; + + if (resource === 'board') { + + if (operation === 'create') { + // ---------------------------------- + // create + // ---------------------------------- + + requestMethod = 'POST'; + endpoint = 'boards'; + + qs.name = this.getNodeParameter('name', i) as string; + qs.desc = this.getNodeParameter('description', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + } else if (operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + requestMethod = 'DELETE'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `boards/${id}`; + + } else if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `boards/${id}`; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + requestMethod = 'PUT'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `boards/${id}`; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(qs, updateFields); + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + + } else if (resource === 'card') { + + if (operation === 'create') { + // ---------------------------------- + // create + // ---------------------------------- + + requestMethod = 'POST'; + endpoint = 'cards'; + + qs.idList = this.getNodeParameter('listId', i) as string; + + qs.name = this.getNodeParameter('name', i) as string; + qs.desc = this.getNodeParameter('description', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + } else if (operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + requestMethod = 'DELETE'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `cards/${id}`; + + } else if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `cards/${id}`; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + requestMethod = 'PUT'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `cards/${id}`; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(qs, updateFields); + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + + + } else if (resource === 'list') { + + if (operation === 'archive') { + // ---------------------------------- + // archive + // ---------------------------------- + + requestMethod = 'PUT'; + + const id = this.getNodeParameter('id', i) as string; + qs.value = this.getNodeParameter('archive', i) as boolean; + + endpoint = `lists/${id}/closed`; + + } else if (operation === 'create') { + // ---------------------------------- + // create + // ---------------------------------- + + requestMethod = 'POST'; + endpoint = 'lists'; + + qs.idBoard = this.getNodeParameter('idBoard', i) as string; + + qs.name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + } else if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `lists/${id}`; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + requestMethod = 'PUT'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `lists/${id}`; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(qs, updateFields); + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + + returnData.push(responseData as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts new file mode 100644 index 0000000000..d32b3dee90 --- /dev/null +++ b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts @@ -0,0 +1,220 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResonseData, +} from 'n8n-workflow'; + +import { + apiRequest, +} from './GenericFunctions'; + +import { createHmac } from 'crypto'; + + +export class TrelloTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Trello Trigger', + name: 'trelloTrigger', + icon: 'file:trello.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Trello events occure.', + defaults: { + name: 'Trello Trigger', + color: '#026aa7', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'trelloApi', + required: true, + }, + ], + webhooks: [ + { + name: 'setup', + httpMethod: 'GET', + reponseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Model ID', + name: 'id', + type: 'string', + default: '', + placeholder: '4d5ea62fd76aa1136000000c', + required: true, + description: 'ID of the model of which to subscribe to events', + }, + ], + + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + if (this.getWebhookName() === 'setup') { + // Is setup-webhook which only gets used once when + // the webhook gets created so nothing to do. + return true; + } + + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + + const credentials = this.getCredentials('trelloApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + // Webhook got created before so check if it still exists + const endpoint = `tokens/${credentials.apiToken}/webhooks/${webhookData.webhookId}`; + + const responseData = await apiRequest.call(this, 'GET', endpoint, {}); + + if (responseData.data === undefined) { + return false; + } + + for (const existingData of responseData.data) { + if (existingData.id === webhookData.webhookId) { + // The webhook exists already + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + if (this.getWebhookName() === 'setup') { + // Is setup-webhook which only gets used once when + // the webhook gets created so nothing to do. + return true; + } + + const webhookUrl = this.getNodeWebhookUrl('default'); + + const credentials = this.getCredentials('trelloApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const idModel = this.getNodeParameter('id') as string; + + const endpoint = `tokens/${credentials.apiToken}/webhooks`; + + const body = { + description: `n8n Webhook - ${idModel}`, + callbackURL: webhookUrl, + idModel, + }; + + const responseData = await apiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = responseData.id as string; + + return true; + }, + async delete(this: IHookFunctions): Promise { + if (this.getWebhookName() === 'setup') { + // Is setup-webhook which only gets used once when + // the webhook gets created so nothing to do. + return true; + } + + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + const credentials = this.getCredentials('trelloApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = `tokens/${credentials.apiToken}/webhooks/${webhookData.webhookId}`; + + const body = {}; + + try { + await apiRequest.call(this, 'DELETE', endpoint, body); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + + return true; + }, + }, + }; + + + + async webhook(this: IWebhookFunctions): Promise { + const webhookName = this.getWebhookName(); + + if (webhookName === 'setup') { + // Is a create webhook confirmation request + const res = this.getResponseObject(); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } + + const bodyData = this.getBodyData(); + + const credentials = this.getCredentials('trelloApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + // TODO: Check why that does not work as expected even though it gets done as described + // https://developers.trello.com/page/webhooks + // // Check if the request is valid + // const headerData = this.getHeaderData() as IDataObject; + // const webhookUrl = this.getNodeWebhookUrl('default'); + // const checkContent = JSON.stringify(bodyData) + webhookUrl; + // const computedSignature = createHmac('sha1', credentials.oauthSecret as string).update(checkContent).digest('base64'); + // if (headerData['x-trello-webhook'] !== computedSignature) { + // // Signature is not valid so ignore call + // return {}; + // } + + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Trello/trello.png b/packages/nodes-base/nodes/Trello/trello.png new file mode 100644 index 0000000000000000000000000000000000000000..9350bc2bfc476e8fa1e2a1eb614d964e7aaebfac GIT binary patch literal 962 zcmV;z13mnSP)fld+azt$nasmGO)ID~$)uS!6LQZcWVmzYe)pU+_ndQw z=(>(_5)Nf14445kUOPe8_62yiNEH<2 z98xm@%qGtOeurXsc%Ti6>^9hp@H`NkY{$^RHs>s?s)uPLTSOCeE#1bl5|Se-ZRjyn zaAta_@;S+f;8)H71GT0xr056*z3}qZmNyuFCDTYtD&XPF2c%U50~PRlvg?UU3R1Ia z&v3AspI_nbPrwbi~H8&NJBNmY!jtpN2XFZaqm5z4o&|Lt!n6KuEWV)wFvt7 ztdoc)WOO_p#+|+>)x&3>V}ZbbJDaTH*q#VZ?XIMX zjUk+EuD7LUU0B48gPYJZp1|nO6h$P~b96NEpk~{#I*`{bbyYq@s>DpJ{)t-+J0roo z^oRmC4{mtIie_3`o{a<5C5?un?=yP20kh(P0W)9*%zzm%17^Ssc-a~jxf%oJ<$j3* z(=ze(x0$?i@6XecMd!oqz_f^e_;wPVJzr>vox9vKIv%GVe8k{ax=6y(rY;l}m>Nl? zKdpD);nI_Fg#ChP1w|IFO~fUP{YaX1J4yt2F(7$@!RSn;ACw~m3W0S8@S+KYm5DC4 zEiqsQT&BSJ?)%2exR4B~=)kFTo_%4u<0NI(@`%daz&wYpz9?o50Gra){~e=7(!dut zik7wydpNupjiITtA4eOju}ZX7V;nRL&{Xi+^9g8N@qK#fi3iPidPidD9iBpNKh-lQ zekBA$@hUF62aU=(U^+DI;61;H^*k5@SZvM=ekT}FZw{yU28*idQyl$N5 zx=vI@MX06~8@7%)2P_C0Jl^A|@U_!_BUt%UPV<;0cm}Vp1%mgs-SU#KFX9<617^Ss km;p0j2F!q$Nq!430L(l1CU>l5-T(jq07*qoM6N<$f-(TNWB>pF literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d4a820345e..ea31bb63b5 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -41,6 +41,7 @@ "dist/credentials/Redis.credentials.js", "dist/credentials/SlackApi.credentials.js", "dist/credentials/Smtp.credentials.js", + "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js" ], "nodes": [ @@ -84,6 +85,8 @@ "dist/nodes/Slack/Slack.node.js", "dist/nodes/SpreadsheetFile.node.js", "dist/nodes/Start.node.js", + "dist/nodes/Trello/Trello.node.js", + "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js"