nodes/YouTrack/YoutrackTrigger.node.ts (226 lines of code) (raw):
import {
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookFunctions,
IWebhookResponseData,
IHookFunctions,
} from 'n8n-workflow';
// Define all available YouTrack events
const YOUTRACK_EVENTS = [
{
name: 'Issue Created',
value: 'issueCreated',
description: 'Trigger when a new issue is created',
},
{
name: 'Issue Updated',
value: 'issueUpdated',
description: 'Trigger when an issue is updated',
},
{
name: 'Issue Deleted',
value: 'issueDeleted',
description: 'Trigger when an issue is deleted',
},
{
name: 'Comment Added',
value: 'commentAdded',
description: 'Trigger when a new comment is added to an issue',
},
{
name: 'Comment Updated',
value: 'commentUpdated',
description: 'Trigger when a new comment is updated',
},
{
name: 'Comment Deleted',
value: 'commentDeleted',
description: 'Trigger when a comment is deleted',
},
{
name: 'Work Item Added',
value: 'workItemAdded',
description: 'Trigger when a work item is added to an issue',
},
{
name: 'Work Item Updated',
value: 'workItemUpdated',
description: 'Trigger when a work item is updated',
},
{
name: 'Work Item Deleted',
value: 'workItemDeleted',
description: 'Trigger when a work item is deleted',
},
{
name: 'Issue Attachment Added',
value: 'issueAttachmentAdded',
description: 'Trigger when an attachment is added to an issue',
},
{
name: 'Issue Attachment Deleted',
value: 'issueAttachmentDeleted',
description: 'Trigger when an attachment is deleted from an issue',
},
] as const;
// Extract event values for validation
const EVENT_VALUES: string[] = YOUTRACK_EVENTS.map(e => e.value);
export class YoutrackTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'YouTrack Trigger',
name: 'youtrackTrigger',
icon: 'file:../../common/youtrack-logo.svg',
group: ['trigger'],
version: 2,
subtitle: '={{$parameter["events"] && $parameter["events"].length > 0 ? $parameter["events"].join(", ") : "No events selected"}}',
description: 'Triggers workflow on YouTrack events',
defaults: {
name: 'YouTrack Trigger',
},
inputs: [],
outputs: ['main'],
usableAsTool: undefined,
credentials: [
{
name: 'permanentTokenCredentialForYouTrackApi',
required: true,
displayName: 'Credentials to Connect to YouTrack',
},
{
name: 'youTrackWebhookAuthApi',
required: true,
displayName: 'Authenticate Incoming Webhook',
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
required: true,
default: ['issueCreated'],
options: [
{
name: '* All Events',
value: '*',
description: 'Trigger on any YouTrack event',
},
...YOUTRACK_EVENTS,
],
description: 'Select one or more events to listen to. Choose "* All Events" to listen to all events.',
},
]
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
return !!webhookData.webhookUrl;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookUrl = webhookUrl;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
delete webhookData.webhookUrl;
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
const selectedEvents = this.getNodeParameter('events', []) as string[];
// Validate authentication if webhook credential is configured
try {
const webhookCredentials = await this.getCredentials('youTrackWebhookAuthApi');
if (webhookCredentials) {
const authMethod = webhookCredentials.authMethod as string;
const expectedToken = webhookCredentials.authToken as string;
let receivedToken: string | undefined;
if (authMethod === 'headerAuth') {
// Header-based authentication
const headerName = webhookCredentials.headerName as string;
receivedToken = req.headers[headerName.toLowerCase()] as string;
} else if (authMethod === 'queryAuth') {
// Query parameter authentication
const queryParamName = webhookCredentials.queryParameterName as string;
receivedToken = req.query?.[queryParamName] as string;
}
// Validate the token
if (!receivedToken || receivedToken !== expectedToken) {
return {
webhookResponse: {
message: 'Unauthorized: Invalid or missing authentication token',
statusCode: 401,
},
};
}
}
} catch (error) {
return {
webhookResponse: {
message: `Error validating authentication: ${error}`,
statusCode: 401,
},
};
}
// Parse the webhook payload safely
let body: IDataObject = {};
try {
body = req.body as IDataObject || {};
} catch (error) {
return {
webhookResponse: {
error: `Invalid payload format. ${error}`,
statusCode: 400,
},
};
}
// Determine the event type from the payload
let detectedEvent = 'unknown';
if (body && typeof body === 'object') {
// Check for the explicit 'event' field from YouTrack app workflows
if (body.event && typeof body.event === 'string') {
const eventValue = body.event as string;
// Validate against known events - automatically includes all events from YOUTRACK_EVENTS
if (EVENT_VALUES.includes(eventValue)) {
detectedEvent = eventValue;
}
}
}
// Check if this event matches our configured event filter
const acceptsAllEvents = selectedEvents && selectedEvents.length > 0 && selectedEvents.includes('*');
// If specific events are selected, verify the detected event is in the list
if (!acceptsAllEvents) {
// Ensure selectedEvents is an array and not empty
if (!selectedEvents || selectedEvents.length === 0) {
return {
webhookResponse: {
message: 'No events selected',
statusCode: 200,
},
};
}
// Check if detected event is in the selected events
if (!selectedEvents.includes(detectedEvent)) {
return {
webhookResponse: {
message: `Event type '${detectedEvent}' does not match filter. Selected events: ${selectedEvents.join(', ')}`,
statusCode: 200,
},
};
}
}
return {
workflowData: [
[
{
json: body,
},
],
],
webhookResponse: {
message: 'Webhook received successfully',
statusCode: 200,
},
};
}
}