Add Taiga Integration (#939)

* Add Taiga integration

*  Improvements to Taiga-Node

*  Improvements

*  Small improvements

*  Improvements

*  Change project slug for project id

*  Small improvement

Co-authored-by: renanfilipe <renanfilipe10@hotmail.com>
This commit is contained in:
Ricardo Espinoza 2020-09-17 17:20:12 -04:00 committed by GitHub
parent 1461e08478
commit bbfe59c211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1137 additions and 0 deletions

View file

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class TaigaCloudApi implements ICredentialType {
name = 'taigaCloudApi';
displayName = 'Taiga Cloud API';
properties = [
{
displayName: 'Username',
name: 'username',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,30 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class TaigaServerApi implements ICredentialType {
name = 'taigaServerApi';
displayName = 'Taiga Server API';
properties = [
{
displayName: 'Username',
name: 'username',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'URL',
name: 'url',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://taiga.yourdomain.com',
},
];
}

View file

@ -0,0 +1,129 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
IDataObject,
} from 'n8n-workflow';
import {
createHash,
} from 'crypto';
export async function getAuthorization(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions,
credentials?: ICredentialDataDecryptedObject,
): Promise<string> {
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const { password, username } = credentials;
const options: OptionsWithUri = {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: {
type: 'normal',
password,
username,
},
uri: (credentials.url) ? `${credentials.url}/api/v1/auth` : 'https://api.taiga.io/api/v1/auth',
json: true,
};
try {
const response = await this.helpers.request!(options);
return response.auth_token;
} catch (error) {
throw new Error('Taiga Error: ' + error.err || error);
}
}
export async function taigaApiRequest(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions,
method: string,
resource: string,
body = {},
query = {},
uri?: string | undefined,
option = {},
): Promise<any> { // tslint:disable-line:no-any
const version = this.getNodeParameter('version', 0, 'cloud') as string;
let credentials;
if (version === 'server') {
credentials = this.getCredentials('taigaServerApi') as ICredentialDataDecryptedObject;
} else {
credentials = this.getCredentials('taigaCloudApi') as ICredentialDataDecryptedObject;
}
const authToken = await getAuthorization.call(this, credentials);
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
auth: {
bearer: authToken,
},
qs: query,
method,
body,
uri: uri || (credentials.url) ? `${credentials.url}/api/v1${resource}` : `https://api.taiga.io/api/v1${resource}`,
json: true
};
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessage = error;
if (error.response.body && error.response.body._error_message) {
errorMessage = error.response.body._error_message;
}
throw new Error(`Taigan error response [${error.statusCode}]: ${errorMessage}`);
}
}
export async function taigaApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
do {
responseData = await taigaApiRequest.call(this, method, resource, body, query, uri, { resolveWithFullResponse: true });
returnData.push.apply(returnData, responseData.body);
uri = responseData.headers['x-pagination-next'];
if (query.limit && returnData.length >= query.limit) {
return returnData;
}
} while (
responseData.headers['x-pagination-next'] !== undefined &&
responseData.headers['x-pagination-next'] !== ''
);
return returnData;
}
export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) {
const data = `${credentials.username},${credentials.password}`;
return createHash('md5').update(data).digest('hex');
}

View file

@ -0,0 +1,40 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const issueOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Create',
value: 'create',
description: 'Create an issue',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an issue',
},
{
name: 'Get',
value: 'get',
description: 'Get an issue',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all issues',
},
{
name: 'Update',
value: 'update',
description: 'Update an issue',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];

View file

@ -0,0 +1,329 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
taigaApiRequest,
taigaApiRequestAllItems,
} from './GenericFunctions';
import {
issueOperations,
} from './IssueOperations';
import {
issueOperationFields,
} from './issueOperationFields';
export class Taiga implements INodeType {
description: INodeTypeDescription = {
displayName: 'Taiga',
name: 'taiga',
icon: 'file:taiga.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Taiga API',
defaults: {
name: 'Taiga',
color: '#772244',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'taigaCloudApi',
displayOptions: {
show: {
version: [
'cloud',
],
},
},
required: true,
},
{
name: 'taigaServerApi',
displayOptions: {
show: {
version: [
'server',
],
},
},
required: true,
},
],
properties: [
{
displayName: 'Taiga Version',
name: 'version',
type: 'options',
options: [
{
name: 'Cloud',
value: 'cloud',
},
{
name: 'Server (Self Hosted)',
value: 'server',
},
],
default: 'cloud',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Issue',
value: 'issue',
},
],
default: 'issue',
description: 'Resource to consume.',
},
...issueOperations,
...issueOperationFields,
],
};
methods = {
loadOptions: {
// Get all the available tags to display them to user so that he can
// select them easily
async getTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const projectId = this.getCurrentNodeParameter('projectId') as string;
const returnData: INodePropertyOptions[] = [];
const types = await taigaApiRequest.call(this, 'GET', `/issue-types?project=${projectId}`);
for (const type of types) {
const typeName = type.name;
const typeId = type.id;
returnData.push({
name: typeName,
value: typeId,
});
}
return returnData;
},
// Get all the available statuses to display them to user so that he can
// select them easily
async getStatuses(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const projectId = this.getCurrentNodeParameter('projectId') as string;
const statuses = await taigaApiRequest.call(this,'GET', '/issue-statuses', {}, { project: projectId });
for (const status of statuses) {
const statusName = status.name;
const statusId = status.id;
returnData.push({
name: statusName,
value: statusId,
});
}
return returnData;
},
// Get all the available users to display them to user so that he can
// select them easily
async getProjectUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const projectId = this.getCurrentNodeParameter('projectId') as string;
const users = await taigaApiRequest.call(this,'GET', '/users', {}, { project: projectId });
for (const user of users) {
const userName = user.username;
const userId = user.id;
returnData.push({
name: userName,
value: userId,
});
}
return returnData;
},
// Get all the available priorities to display them to user so that he can
// select them easily
async getProjectPriorities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const projectId = this.getCurrentNodeParameter('projectId') as string;
const priorities = await taigaApiRequest.call(this,'GET', '/priorities', {}, { project: projectId });
for (const priority of priorities) {
const priorityName = priority.name;
const priorityId = priority.id;
returnData.push({
name: priorityName,
value: priorityId,
});
}
return returnData;
},
// Get all the available severities to display them to user so that he can
// select them easily
async getProjectSeverities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const projectId = this.getCurrentNodeParameter('projectId') as string;
const severities = await taigaApiRequest.call(this,'GET', '/severities', {}, { project: projectId });
for (const severity of severities) {
const severityName = severity.name;
const severityId = severity.id;
returnData.push({
name: severityName,
value: severityId,
});
}
return returnData;
},
// Get all the available milestones to display them to user so that he can
// select them easily
async getProjectMilestones(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const projectId = this.getCurrentNodeParameter('projectId') as string;
const milestones = await taigaApiRequest.call(this,'GET', '/milestones', {}, { project: projectId });
for (const milestone of milestones) {
const milestoneName = milestone.name;
const milestoneId = milestone.id;
returnData.push({
name: milestoneName,
value: milestoneId,
});
}
return returnData;
},
// Get all the available projects to display them to user so that he can
// select them easily
async getUserProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { id } = await taigaApiRequest.call(this, 'GET', '/users/me');
const projects = await taigaApiRequest.call(this,'GET', '/projects', {}, { member: id });
for (const project of projects) {
const projectName = project.name;
const projectId = project.id;
returnData.push({
name: projectName,
value: projectId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
const qs: IDataObject = {};
for (let i = 0; i < items.length; i++) {
if (resource === 'issue') {
if (operation === 'create') {
const projectId = this.getNodeParameter('projectId', i) as number;
const subject = this.getNodeParameter('subject', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
project: projectId,
subject,
};
Object.assign(body, additionalFields);
if (body.tags) {
body.tags = (body.tags as string).split(',') as string[];
}
responseData = await taigaApiRequest.call(this, 'POST', '/issues', body);
}
if (operation === 'update') {
const issueId = this.getNodeParameter('issueId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {};
Object.assign(body, updateFields);
if (body.tags) {
body.tags = (body.tags as string).split(',') as string[];
}
const { version } = await taigaApiRequest.call(this, 'GET', `/issues/${issueId}`);
body.version = version;
responseData = await taigaApiRequest.call(this, 'PATCH', `/issues/${issueId}`, body);
}
if (operation === 'delete') {
const issueId = this.getNodeParameter('issueId', i) as string;
responseData = await taigaApiRequest.call(this, 'DELETE', `/issues/${issueId}`);
responseData = { success: true };
}
if (operation === 'get') {
const issueId = this.getNodeParameter('issueId', i) as string;
responseData = await taigaApiRequest.call(this, 'GET', `/issues/${issueId}`);
}
if (operation === 'getAll') {
const projectId = this.getNodeParameter('projectId', i) as number;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
qs.project = projectId;
if (returnAll === true) {
responseData = await taigaApiRequestAllItems.call(this, 'GET', '/issues', {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await taigaApiRequestAllItems.call(this, 'GET', '/issues', {}, qs);
responseData = responseData.splice(0, qs.limit);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,225 @@
import {
ICredentialDataDecryptedObject,
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookFunctions,
IWebhookResponseData,
} from 'n8n-workflow';
import {
IHookFunctions,
} from 'n8n-core';
import {
taigaApiRequest,
getAutomaticSecret,
} from './GenericFunctions';
// import {
// createHmac,
// } from 'crypto';
export class TaigaTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Taiga Trigger',
name: 'taigaTrigger',
icon: 'file:taiga.png',
group: ['trigger'],
version: 1,
subtitle: '={{"project:" + $parameter["projectSlug"]}}',
description: 'Handle Taiga events via webhook',
defaults: {
name: 'Taiga Trigger',
color: '#772244',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'taigaCloudApi',
displayOptions: {
show: {
version: [
'cloud',
],
},
},
required: true,
},
{
name: 'taigaServerApi',
displayOptions: {
show: {
version: [
'server',
],
},
},
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Taiga Version',
name: 'version',
type: 'options',
options: [
{
name: 'Cloud',
value: 'cloud',
},
{
name: 'Server (Self Hosted)',
value: 'server',
},
],
default: 'cloud',
},
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUserProjects',
},
default: '',
description: 'Project ID',
required: true,
},
],
};
methods = {
loadOptions: {
// Get all the available projects to display them to user so that he can
// select them easily
async getUserProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { id } = await taigaApiRequest.call(this, 'GET', '/users/me');
const projects = await taigaApiRequest.call(this,'GET', '/projects', {}, { member: id });
for (const project of projects) {
const projectName = project.name;
const projectId = project.id;
returnData.push({
name: projectName,
value: projectId,
});
}
return returnData;
},
},
};
// @ts-ignore
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const endpoint = `/webhooks`;
const webhooks = await taigaApiRequest.call(this, 'GET', endpoint);
for (const webhook of webhooks) {
if (webhook.url === webhookUrl) {
webhookData.webhookId = webhook.id;
webhookData.key = webhook.key;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const version = this.getNodeParameter('version') as string;
let credentials;
if (version === 'server') {
credentials = this.getCredentials('taigaServerApi') as ICredentialDataDecryptedObject;
} else {
credentials = this.getCredentials('taigaCloudApi') as ICredentialDataDecryptedObject;
}
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const projectId = this.getNodeParameter('projectId') as string;
const key = getAutomaticSecret(credentials);
const body: IDataObject = {
name: `n8n-webhook:${webhookUrl}`,
url: webhookUrl,
key, //can't validate the secret, see: https://github.com/taigaio/taiga-back/issues/1031
project: projectId,
};
const { id } = await taigaApiRequest.call(this, 'POST', '/webhooks', body);
webhookData.webhookId = id;
webhookData.key = key;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
try {
await taigaApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.webhookId}`);
} catch(error) {
return false;
}
delete webhookData.webhookId;
delete webhookData.key;
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
//const webhookData = this.getWorkflowStaticData('node');
const req = this.getRequestObject();
const bodyData = req.body;
//const headerData = this.getHeaderData();
// TODO
// Validate signature
// https://github.com/taigaio/taiga-back/issues/1031
// //@ts-ignore
// const requestSignature: string = headerData['x-taiga-webhook-signature'];
// if (requestSignature === undefined) {
// return {};
// }
// //@ts-ignore
// const computedSignature = createHmac('sha1', webhookData.key as string).update(JSON.stringify(bodyData)).digest('hex');
// if (requestSignature !== computedSignature) {
// return {};
// }
return {
workflowData: [
this.helpers.returnJsonArray(bodyData)
],
};
}
}

View file

@ -0,0 +1,357 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const issueOperationFields = [
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUserProjects',
},
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'create',
'getAll',
'update',
],
},
},
default: '',
description: 'The project ID.',
required: true,
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'create',
],
},
},
default: '',
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Assigned To',
name: 'assigned_to',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectId',
],
loadOptionsMethod: 'getProjectUsers',
},
default: '',
description: 'User id to you want assign the issue to',
},
{
displayName: 'Blocked Note',
name: 'blocked_note',
type: 'string',
default: '',
description: 'Reason why the issue is blocked',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
},
{
displayName: 'Is Blocked',
name: 'is_blocked',
type: 'boolean',
default: false,
},
{
displayName: 'Is Closed',
name: 'is_closed',
type: 'boolean',
default: false,
},
{
displayName: 'Milestone ID',
name: 'milestone',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getProjectMilestones',
},
default: '',
},
{
displayName: 'Priority ID',
name: 'priority',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getProjectPriorities',
},
default: '',
},
{
displayName: 'Severity ID',
name: 'severity',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getProjectSeverities',
},
default: '',
},
{
displayName: 'Status ID',
name: 'status',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getStatuses',
},
default: '',
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
description: 'Tags separated by comma.',
default: '',
placeholder: 'product, sales',
},
{
displayName: 'Type ID',
name: 'type',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getTypes'
},
default: '',
},
],
},
{
displayName: 'Issue ID',
name: 'issueId',
type: 'string',
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'update',
'delete',
'get',
],
},
},
default: '',
required: true,
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Assigned To',
name: 'assigned_to',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectId',
],
loadOptionsMethod: 'getProjectUsers',
},
default: '',
description: 'User id to you want assign the issue to',
},
{
displayName: 'Blocked Note',
name: 'blocked_note',
type: 'string',
default: '',
description: 'Reason why the issue is blocked',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
},
{
displayName: 'Is Blocked',
name: 'is_blocked',
type: 'boolean',
default: false,
},
{
displayName: 'Is Closed',
name: 'is_closed',
type: 'boolean',
default: false,
},
{
displayName: 'Milestone ID',
name: 'milestone',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getProjectMilestones',
},
default: '',
},
{
displayName: 'Priority ID',
name: 'priority',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getProjectPriorities',
},
default: '',
},
{
displayName: 'Severity ID',
name: 'severity',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getProjectSeverities',
},
default: '',
},
{
displayName: 'Status ID',
name: 'status',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getStatuses',
},
default: '',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
description: 'Tags separated by comma.',
default: '',
placeholder: 'product, sales',
},
{
displayName: 'Type ID',
name: 'type',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'projectSlug',
],
loadOptionsMethod: 'getTypes'
},
default: '',
},
],
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'issue',
],
},
},
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: [
'issue',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -157,6 +157,8 @@
"dist/credentials/SpotifyOAuth2Api.credentials.js",
"dist/credentials/SurveyMonkeyApi.credentials.js",
"dist/credentials/SurveyMonkeyOAuth2Api.credentials.js",
"dist/credentials/TaigaCloudApi.credentials.js",
"dist/credentials/TaigaServerApi.credentials.js",
"dist/credentials/TelegramApi.credentials.js",
"dist/credentials/TodoistApi.credentials.js",
"dist/credentials/TodoistOAuth2Api.credentials.js",
@ -342,6 +344,8 @@
"dist/nodes/Salesmate/Salesmate.node.js",
"dist/nodes/Segment/Segment.node.js",
"dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js",
"dist/nodes/Taiga/Taiga.node.js",
"dist/nodes/Taiga/TaigaTrigger.node.js",
"dist/nodes/Telegram/Telegram.node.js",
"dist/nodes/Telegram/TelegramTrigger.node.js",
"dist/nodes/Todoist/Todoist.node.js",