From 62e645fc6d80fc42cfbe0bd69da08c2864aedf2a Mon Sep 17 00:00:00 2001 From: seynadio <79858321+seynadio@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:47:12 +0200 Subject: [PATCH] Complete Freshservice integration to match Freshdesk functionality - Add comprehensive app with all prop definitions and API methods - Add constants and utilities for consistent data handling - Add all 11 actions matching Freshdesk: * create-ticket, update-ticket (with internal notes support) * get-ticket, list-all-tickets * create-contact, create-company * assign-ticket-to-agent, assign-ticket-to-group * close-ticket, set-ticket-priority, set-ticket-status - Add internal notes support in update-ticket action - Enhance new-ticket source with proper date filtering and pagination - Add new-contact source with complete implementation - Update package.json with required dependencies (moment, async-retry) - Full parity with Freshdesk integration functionality --- .../assign-ticket-to-agent.mjs | 37 +++ .../assign-ticket-to-group.mjs | 37 +++ .../actions/close-ticket/close-ticket.mjs | 31 +++ .../actions/create-company/create-company.mjs | 109 ++++++++ .../actions/create-contact/create-contact.mjs | 110 ++++++++ .../actions/create-ticket/create-ticket.mjs | 142 ++++++++++ .../actions/get-ticket/get-ticket.mjs | 27 ++ .../list-all-tickets/list-all-tickets.mjs | 55 ++++ .../set-ticket-priority.mjs | 39 +++ .../set-ticket-status/set-ticket-status.mjs | 39 +++ .../actions/update-ticket/update-ticket.mjs | 163 +++++++++++ components/freshservice/common/constants.mjs | 72 +++++ components/freshservice/common/utils.mjs | 29 ++ components/freshservice/freshservice.app.mjs | 259 +++++++++++++++++- components/freshservice/package.json | 6 +- .../sources/new-contact/new-contact.mjs | 84 ++++++ .../sources/new-ticket/new-ticket.mjs | 60 +++- 17 files changed, 1284 insertions(+), 15 deletions(-) create mode 100644 components/freshservice/actions/assign-ticket-to-agent/assign-ticket-to-agent.mjs create mode 100644 components/freshservice/actions/assign-ticket-to-group/assign-ticket-to-group.mjs create mode 100644 components/freshservice/actions/close-ticket/close-ticket.mjs create mode 100644 components/freshservice/actions/create-company/create-company.mjs create mode 100644 components/freshservice/actions/create-contact/create-contact.mjs create mode 100644 components/freshservice/actions/create-ticket/create-ticket.mjs create mode 100644 components/freshservice/actions/get-ticket/get-ticket.mjs create mode 100644 components/freshservice/actions/list-all-tickets/list-all-tickets.mjs create mode 100644 components/freshservice/actions/set-ticket-priority/set-ticket-priority.mjs create mode 100644 components/freshservice/actions/set-ticket-status/set-ticket-status.mjs create mode 100644 components/freshservice/actions/update-ticket/update-ticket.mjs create mode 100644 components/freshservice/common/constants.mjs create mode 100644 components/freshservice/common/utils.mjs create mode 100644 components/freshservice/sources/new-contact/new-contact.mjs diff --git a/components/freshservice/actions/assign-ticket-to-agent/assign-ticket-to-agent.mjs b/components/freshservice/actions/assign-ticket-to-agent/assign-ticket-to-agent.mjs new file mode 100644 index 0000000000000..afb9ce4634495 --- /dev/null +++ b/components/freshservice/actions/assign-ticket-to-agent/assign-ticket-to-agent.mjs @@ -0,0 +1,37 @@ +import freshservice from "../../freshservice.app.mjs"; + +export default { + key: "freshservice-assign-ticket-to-agent", + name: "Assign Ticket to Agent", + description: "Assign a ticket to an agent in Freshservice. [See the documentation](https://api.freshservice.com/v2/#update_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + responder_id: { + propDefinition: [ + freshservice, + "agentId", + ], + }, + }, + async run({ $ }) { + const response = await this.freshservice.updateTicket({ + ticketId: this.ticketId, + data: { + responder_id: this.responder_id, + }, + $, + }); + + const ticketName = await this.freshservice.getTicketName(this.ticketId); + $.export("$summary", `Successfully assigned ticket "${ticketName}" to agent`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/assign-ticket-to-group/assign-ticket-to-group.mjs b/components/freshservice/actions/assign-ticket-to-group/assign-ticket-to-group.mjs new file mode 100644 index 0000000000000..b1e501118fb30 --- /dev/null +++ b/components/freshservice/actions/assign-ticket-to-group/assign-ticket-to-group.mjs @@ -0,0 +1,37 @@ +import freshservice from "../../freshservice.app.mjs"; + +export default { + key: "freshservice-assign-ticket-to-group", + name: "Assign Ticket to Group", + description: "Assign a ticket to a group in Freshservice. [See the documentation](https://api.freshservice.com/v2/#update_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + group_id: { + propDefinition: [ + freshservice, + "groupId", + ], + }, + }, + async run({ $ }) { + const response = await this.freshservice.updateTicket({ + ticketId: this.ticketId, + data: { + group_id: this.group_id, + }, + $, + }); + + const ticketName = await this.freshservice.getTicketName(this.ticketId); + $.export("$summary", `Successfully assigned ticket "${ticketName}" to group`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/close-ticket/close-ticket.mjs b/components/freshservice/actions/close-ticket/close-ticket.mjs new file mode 100644 index 0000000000000..a10fd0dafa58f --- /dev/null +++ b/components/freshservice/actions/close-ticket/close-ticket.mjs @@ -0,0 +1,31 @@ +import freshservice from "../../freshservice.app.mjs"; + +export default { + key: "freshservice-close-ticket", + name: "Close Ticket", + description: "Close a ticket in Freshservice. [See the documentation](https://api.freshservice.com/v2/#update_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + }, + async run({ $ }) { + const response = await this.freshservice.updateTicket({ + ticketId: this.ticketId, + data: { + status: 5, // Closed + }, + $, + }); + + const ticketName = await this.freshservice.getTicketName(this.ticketId); + $.export("$summary", `Successfully closed ticket "${ticketName}"`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/create-company/create-company.mjs b/components/freshservice/actions/create-company/create-company.mjs new file mode 100644 index 0000000000000..ad5979cc436ca --- /dev/null +++ b/components/freshservice/actions/create-company/create-company.mjs @@ -0,0 +1,109 @@ +import freshservice from "../../freshservice.app.mjs"; +import { removeNullEntries } from "../../common/utils.mjs"; + +export default { + key: "freshservice-create-company", + name: "Create Company", + description: "Create a new company in Freshservice. [See the documentation](https://api.freshservice.com/v2/#create_company)", + version: "0.0.1", + type: "action", + props: { + freshservice, + name: { + type: "string", + label: "Name", + description: "Name of the company", + }, + description: { + type: "string", + label: "Description", + description: "Description of the company", + optional: true, + }, + note: { + type: "string", + label: "Note", + description: "Note about the company", + optional: true, + }, + domains: { + type: "string[]", + label: "Domains", + description: "Domains associated with the company", + optional: true, + }, + primary_email: { + type: "string", + label: "Primary Email", + description: "Primary email of the company", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the company", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Address of the company", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the company", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State of the company", + optional: true, + }, + zip_code: { + type: "string", + label: "Zip Code", + description: "Zip code of the company", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country of the company", + optional: true, + }, + custom_fields: { + type: "object", + label: "Custom Fields", + description: "Custom fields as a JSON object", + optional: true, + }, + }, + async run({ $ }) { + const { + custom_fields, + domains, + ...otherProps + } = this; + + const data = removeNullEntries(otherProps); + + if (custom_fields) { + data.custom_fields = this.freshservice.parseIfJSONString(custom_fields); + } + + if (domains && domains.length > 0) { + data.domains = domains; + } + + const response = await this.freshservice.createCompany({ + data, + $, + }); + + $.export("$summary", `Successfully created company: ${response.company?.name}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/create-contact/create-contact.mjs b/components/freshservice/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..3495b0d139bbc --- /dev/null +++ b/components/freshservice/actions/create-contact/create-contact.mjs @@ -0,0 +1,110 @@ +import { ConfigurationError } from "@pipedream/platform"; +import freshservice from "../../freshservice.app.mjs"; +import { removeNullEntries } from "../../common/utils.mjs"; + +export default { + key: "freshservice-create-contact", + name: "Create Contact", + description: "Create a new contact in Freshservice. [See the documentation](https://api.freshservice.com/v2/#create_requester)", + version: "0.0.1", + type: "action", + props: { + freshservice, + first_name: { + type: "string", + label: "First Name", + description: "First name of the contact", + }, + last_name: { + type: "string", + label: "Last Name", + description: "Last name of the contact", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Primary email address of the contact", + optional: true, + }, + secondary_emails: { + type: "string[]", + label: "Secondary Emails", + description: "Additional email addresses of the contact", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the contact", + optional: true, + }, + mobile: { + type: "string", + label: "Mobile", + description: "Mobile number of the contact", + optional: true, + }, + job_title: { + type: "string", + label: "Job Title", + description: "Job title of the contact", + optional: true, + }, + department_id: { + type: "string", + label: "Department ID", + description: "Department ID of the contact", + optional: true, + }, + company_id: { + propDefinition: [ + freshservice, + "companyId", + ], + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Address of the contact", + optional: true, + }, + time_zone: { + type: "string", + label: "Time Zone", + description: "Time zone of the contact", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "Language of the contact", + optional: true, + }, + }, + async run({ $ }) { + const { + secondary_emails, + ...otherProps + } = this; + + if (!this.email && !this.phone) { + throw new ConfigurationError("Either email or phone must be provided"); + } + + const data = removeNullEntries(otherProps); + + if (secondary_emails && secondary_emails.length > 0) { + data.secondary_emails = secondary_emails; + } + + const response = await this.freshservice.createContact({ + data, + $, + }); + + $.export("$summary", `Successfully created contact: ${response.requester?.first_name} ${response.requester?.last_name || ""}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/create-ticket/create-ticket.mjs b/components/freshservice/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..9c2603e160d0d --- /dev/null +++ b/components/freshservice/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,142 @@ +import freshservice from "../../freshservice.app.mjs"; +import { removeNullEntries } from "../../common/utils.mjs"; + +export default { + key: "freshservice-create-ticket", + name: "Create Ticket", + description: "Create a new ticket in Freshservice. [See the documentation](https://api.freshservice.com/v2/#create_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + subject: { + type: "string", + label: "Subject", + description: "Subject of the ticket", + }, + description: { + type: "string", + label: "Description", + description: "Description of the ticket", + }, + email: { + type: "string", + label: "Email", + description: "Email of the requester", + }, + priority: { + propDefinition: [ + freshservice, + "ticketPriority", + ], + optional: true, + }, + status: { + propDefinition: [ + freshservice, + "ticketStatus", + ], + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the requester", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name of the requester", + optional: true, + }, + company_id: { + propDefinition: [ + freshservice, + "companyId", + ], + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type of the ticket", + options: [ + "Incident", + "Service Request", + "Change", + "Problem", + "Release", + ], + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "Source of the ticket", + options: [ + "Email", + "Portal", + "Phone", + "Chat", + "Feedback Widget", + "Yammer", + "AWS Cloudwatch", + "Pagerduty", + "Walkup", + "Slack", + ], + optional: true, + }, + urgency: { + type: "string", + label: "Urgency", + description: "Urgency of the ticket", + options: [ + "Low", + "Medium", + "High", + "Critical", + ], + optional: true, + }, + impact: { + type: "string", + label: "Impact", + description: "Impact of the ticket", + options: [ + "Low", + "Medium", + "High", + "Critical", + ], + optional: true, + }, + custom_fields: { + type: "object", + label: "Custom Fields", + description: "Custom fields as a JSON object", + optional: true, + }, + }, + async run({ $ }) { + const { + custom_fields, + ...otherProps + } = this; + + const data = removeNullEntries(otherProps); + + if (custom_fields) { + data.custom_fields = this.freshservice.parseIfJSONString(custom_fields); + } + + const response = await this.freshservice.createTicket({ + data, + $, + }); + + $.export("$summary", `Successfully created ticket: ${response.ticket?.subject || response.ticket?.id}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/get-ticket/get-ticket.mjs b/components/freshservice/actions/get-ticket/get-ticket.mjs new file mode 100644 index 0000000000000..ecee11e8c2809 --- /dev/null +++ b/components/freshservice/actions/get-ticket/get-ticket.mjs @@ -0,0 +1,27 @@ +import freshservice from "../../freshservice.app.mjs"; + +export default { + key: "freshservice-get-ticket", + name: "Get Ticket", + description: "Get details of a ticket in Freshservice. [See the documentation](https://api.freshservice.com/v2/#view_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + }, + async run({ $ }) { + const response = await this.freshservice.getTicket({ + ticketId: this.ticketId, + $, + }); + + $.export("$summary", `Successfully retrieved ticket: ${response.ticket?.subject || this.ticketId}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/list-all-tickets/list-all-tickets.mjs b/components/freshservice/actions/list-all-tickets/list-all-tickets.mjs new file mode 100644 index 0000000000000..7fa6382267d06 --- /dev/null +++ b/components/freshservice/actions/list-all-tickets/list-all-tickets.mjs @@ -0,0 +1,55 @@ +import freshservice from "../../freshservice.app.mjs"; + +export default { + key: "freshservice-list-all-tickets", + name: "List All Tickets", + description: "List all tickets in Freshservice. [See the documentation](https://api.freshservice.com/v2/#list_all_tickets)", + version: "0.0.1", + type: "action", + props: { + freshservice, + orderBy: { + propDefinition: [ + freshservice, + "orderBy", + ], + optional: true, + }, + orderType: { + propDefinition: [ + freshservice, + "orderType", + ], + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "Maximum number of tickets to return (max 100)", + optional: true, + default: 30, + max: 100, + }, + }, + async run({ $ }) { + const params = { + per_page: this.limit, + }; + + if (this.orderBy) { + params.order_by = this.orderBy; + } + if (this.orderType) { + params.order_type = this.orderType; + } + + const response = await this.freshservice.listTickets({ + params, + $, + }); + + const tickets = response.tickets || []; + $.export("$summary", `Successfully retrieved ${tickets.length} tickets`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/set-ticket-priority/set-ticket-priority.mjs b/components/freshservice/actions/set-ticket-priority/set-ticket-priority.mjs new file mode 100644 index 0000000000000..da298bf54cffc --- /dev/null +++ b/components/freshservice/actions/set-ticket-priority/set-ticket-priority.mjs @@ -0,0 +1,39 @@ +import freshservice from "../../freshservice.app.mjs"; +import { TICKET_PRIORITY } from "../../common/constants.mjs"; + +export default { + key: "freshservice-set-ticket-priority", + name: "Set Ticket Priority", + description: "Set the priority of a ticket in Freshservice. [See the documentation](https://api.freshservice.com/v2/#update_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + priority: { + propDefinition: [ + freshservice, + "ticketPriority", + ], + }, + }, + async run({ $ }) { + const response = await this.freshservice.updateTicket({ + ticketId: this.ticketId, + data: { + priority: this.priority, + }, + $, + }); + + const ticketName = await this.freshservice.getTicketName(this.ticketId); + const priorityLabel = TICKET_PRIORITY[this.priority]; + $.export("$summary", `Successfully set priority of ticket "${ticketName}" to ${priorityLabel}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/set-ticket-status/set-ticket-status.mjs b/components/freshservice/actions/set-ticket-status/set-ticket-status.mjs new file mode 100644 index 0000000000000..b705d31e1aaa0 --- /dev/null +++ b/components/freshservice/actions/set-ticket-status/set-ticket-status.mjs @@ -0,0 +1,39 @@ +import freshservice from "../../freshservice.app.mjs"; +import { TICKET_STATUS } from "../../common/constants.mjs"; + +export default { + key: "freshservice-set-ticket-status", + name: "Set Ticket Status", + description: "Set the status of a ticket in Freshservice. [See the documentation](https://api.freshservice.com/v2/#update_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + status: { + propDefinition: [ + freshservice, + "ticketStatus", + ], + }, + }, + async run({ $ }) { + const response = await this.freshservice.updateTicket({ + ticketId: this.ticketId, + data: { + status: this.status, + }, + $, + }); + + const ticketName = await this.freshservice.getTicketName(this.ticketId); + const statusLabel = TICKET_STATUS[this.status]; + $.export("$summary", `Successfully set status of ticket "${ticketName}" to ${statusLabel}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/actions/update-ticket/update-ticket.mjs b/components/freshservice/actions/update-ticket/update-ticket.mjs new file mode 100644 index 0000000000000..1200602e8502e --- /dev/null +++ b/components/freshservice/actions/update-ticket/update-ticket.mjs @@ -0,0 +1,163 @@ +import { ConfigurationError } from "@pipedream/platform"; +import freshservice from "../../freshservice.app.mjs"; +import { TICKET_STATUS, TICKET_PRIORITY } from "../../common/constants.mjs"; +import { removeNullEntries } from "../../common/utils.mjs"; + +export default { + key: "freshservice-update-ticket", + name: "Update Ticket", + description: "Update a ticket in Freshservice. Optionally add an internal note instead of updating the ticket. [See the documentation](https://api.freshservice.com/v2/#update_ticket)", + version: "0.0.1", + type: "action", + props: { + freshservice, + ticketId: { + propDefinition: [ + freshservice, + "ticketId", + ], + }, + status: { + propDefinition: [ + freshservice, + "ticketStatus", + ], + optional: true, + }, + priority: { + propDefinition: [ + freshservice, + "ticketPriority", + ], + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Subject of the ticket", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of the ticket", + optional: true, + }, + group_id: { + propDefinition: [ + freshservice, + "groupId", + ], + optional: true, + }, + responder_id: { + propDefinition: [ + freshservice, + "agentId", + ], + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email of the requester", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the requester", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name of the requester", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type of the ticket", + options: [ + "Incident", + "Service Request", + "Change", + "Problem", + "Release", + ], + optional: true, + }, + custom_fields: { + type: "object", + label: "Custom Fields", + description: "Custom fields as a JSON object", + optional: true, + }, + internalNote: { + type: "boolean", + label: "Internal Note", + description: "If true, add an internal note instead of updating the ticket", + optional: true, + default: false, + }, + noteBody: { + type: "string", + label: "Note Body", + description: "Body of the internal note (only used if Internal Note is true)", + optional: true, + }, + }, + async run({ $ }) { + const { + ticketId, + internalNote, + noteBody, + custom_fields, + ...otherProps + } = this; + + // Handle internal note creation + if (internalNote) { + if (!noteBody) { + throw new ConfigurationError("Note Body is required when Internal Note is enabled"); + } + + const noteResponse = await this.freshservice.createNote({ + ticketId, + data: { + body: noteBody, + private: true, + }, + $, + }); + + $.export("$summary", `Successfully added internal note to ticket ${ticketId}`); + return noteResponse; + } + + // Handle ticket update + const data = removeNullEntries(otherProps); + + if (custom_fields) { + data.custom_fields = this.freshservice.parseIfJSONString(custom_fields); + } + + // Validate that at least one field is provided for update + if (Object.keys(data).length === 0) { + throw new ConfigurationError("At least one field must be provided to update the ticket"); + } + + const response = await this.freshservice.updateTicket({ + ticketId, + data, + $, + }); + + const statusLabel = TICKET_STATUS[response.ticket?.status] || response.ticket?.status; + const priorityLabel = TICKET_PRIORITY[response.ticket?.priority] || response.ticket?.priority; + + $.export("$summary", `Successfully updated ticket ${ticketId}${statusLabel ? ` (Status: ${statusLabel})` : ""}${priorityLabel ? ` (Priority: ${priorityLabel})` : ""}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/freshservice/common/constants.mjs b/components/freshservice/common/constants.mjs new file mode 100644 index 0000000000000..ebd1d2a80c03f --- /dev/null +++ b/components/freshservice/common/constants.mjs @@ -0,0 +1,72 @@ +export const PAGE_SIZE = 100; +export const DB_LAST_DATE_CHECK = "lastDateCheck"; + +export const TICKET_STATUS = { + 2: "Open", + 3: "Pending", + 4: "Resolved", + 5: "Closed", +}; + +export const TICKET_PRIORITY = { + 1: "Low", + 2: "Medium", + 3: "High", + 4: "Urgent", +}; + +export const STATUS_OPTIONS = [ + { + label: "Open", + value: 2, + }, + { + label: "Pending", + value: 3, + }, + { + label: "Resolved", + value: 4, + }, + { + label: "Closed", + value: 5, + }, +]; + +export const PRIORITY_OPTIONS = [ + { + label: "Low", + value: 1, + }, + { + label: "Medium", + value: 2, + }, + { + label: "High", + value: 3, + }, + { + label: "Urgent", + value: 4, + }, +]; + +export const TICKET_SORT_OPTIONS = [ + "created_at", + "due_by", + "updated_at", + "status", +]; + +export const ORDER_TYPE_OPTIONS = [ + { + label: "Ascending", + value: "asc", + }, + { + label: "Descending", + value: "desc", + }, +]; \ No newline at end of file diff --git a/components/freshservice/common/utils.mjs b/components/freshservice/common/utils.mjs new file mode 100644 index 0000000000000..30173b1128130 --- /dev/null +++ b/components/freshservice/common/utils.mjs @@ -0,0 +1,29 @@ +function removeNullEntries(obj) { + if (typeof obj === "number") { + return obj; + } + if (typeof obj === "boolean") { + return obj; + } + if (typeof obj === "string") { + return obj; + } + if (Array.isArray(obj)) { + return obj.filter((item) => item !== null && item !== undefined && item !== "") + .map((item) => removeNullEntries(item)); + } + if (typeof obj === "object" && obj !== null) { + const result = {}; + for (const key in obj) { + if (obj[key] !== null && obj[key] !== undefined && obj[key] !== "") { + result[key] = removeNullEntries(obj[key]); + } + } + return result; + } + return obj; +} + +export { + removeNullEntries, +}; \ No newline at end of file diff --git a/components/freshservice/freshservice.app.mjs b/components/freshservice/freshservice.app.mjs index 44f4558e415e2..48bcdb9d47021 100644 --- a/components/freshservice/freshservice.app.mjs +++ b/components/freshservice/freshservice.app.mjs @@ -1,9 +1,133 @@ import { axios } from "@pipedream/platform"; +import { + DB_LAST_DATE_CHECK, + PAGE_SIZE, + STATUS_OPTIONS, + PRIORITY_OPTIONS, + TICKET_SORT_OPTIONS, + ORDER_TYPE_OPTIONS, +} from "./common/constants.mjs"; +import { removeNullEntries } from "./common/utils.mjs"; export default { type: "app", app: "freshservice", - propDefinitions: {}, + propDefinitions: { + companyId: { + type: "string", + label: "Company ID", + description: "ID of the company to which the ticket belongs", + async options({ page }) { + const { companies } = await this.getCompanies({ + params: { + page: page + 1, + per_page: PAGE_SIZE, + }, + }); + return companies?.map((company) => ({ + label: company.name, + value: company.id, + })) || []; + }, + }, + ticketId: { + type: "string", + label: "Ticket ID", + description: "ID of the ticket", + async options({ page }) { + const { tickets } = await this.listTickets({ + params: { + page: page + 1, + per_page: PAGE_SIZE, + }, + }); + return tickets?.map((ticket) => ({ + label: ticket.subject, + value: ticket.id, + })) || []; + }, + }, + agentId: { + type: "string", + label: "Agent ID", + description: "ID of the agent", + async options({ page }) { + const { agents } = await this.getAgents({ + params: { + page: page + 1, + per_page: PAGE_SIZE, + }, + }); + return agents?.map((agent) => ({ + label: `${agent.contact.name} (${agent.contact.email})`, + value: agent.id, + })) || []; + }, + }, + groupId: { + type: "string", + label: "Group ID", + description: "ID of the group", + async options({ page }) { + const { groups } = await this.getGroups({ + params: { + page: page + 1, + per_page: PAGE_SIZE, + }, + }); + return groups?.map((group) => ({ + label: group.name, + value: group.id, + })) || []; + }, + }, + ticketStatus: { + type: "integer", + label: "Status", + description: "Status of the ticket", + options: STATUS_OPTIONS, + }, + ticketPriority: { + type: "integer", + label: "Priority", + description: "Priority of the ticket", + options: PRIORITY_OPTIONS, + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "Email of the contact", + async options({ + page, companyId, + }) { + const contacts = await this.getContacts({ + params: { + page: page + 1, + per_page: PAGE_SIZE, + company_id: companyId, + }, + }); + return contacts?.map((contact) => ({ + label: contact.email, + value: contact.email, + })) || []; + }, + }, + orderBy: { + type: "string", + label: "Order By", + description: "Sort tickets by this field", + options: TICKET_SORT_OPTIONS, + default: "created_at", + }, + orderType: { + type: "string", + label: "Order Type", + description: "Sort order", + options: ORDER_TYPE_OPTIONS, + default: "desc", + }, + }, methods: { _domain() { return this.$auth.domain; @@ -14,11 +138,17 @@ export default { _apiUrl() { return `https://${this._domain()}.freshservice.com/api`; }, + _getHeaders() { + return { + "Content-Type": "application/json", + }; + }, async _makeRequest({ $ = this, path, ...args }) { return axios($, { url: `${this._apiUrl()}${path}`, + headers: this._getHeaders(), auth: { username: this._apiKey(), password: "X", @@ -26,6 +156,133 @@ export default { ...args, }); }, + base64Encode(str) { + return Buffer.from(str).toString("base64"); + }, + parseIfJSONString(value) { + try { + return typeof value === "string" ? JSON.parse(value) : value; + } catch (error) { + return value; + } + }, + async setLastDateChecked(value) { + await this.db.set(DB_LAST_DATE_CHECK, value); + }, + async getLastDateChecked() { + return await this.db.get(DB_LAST_DATE_CHECK) || "2021-01-01T00:00:00Z"; + }, + // Company methods + async createCompany(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/v2/companies", + ...args, + }); + }, + async getCompanies(args = {}) { + return this._makeRequest({ + path: "/v2/companies", + ...args, + }); + }, + // Contact methods + async createContact(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/v2/requesters", + ...args, + }); + }, + async getContacts(args = {}) { + return this._makeRequest({ + path: "/v2/requesters", + ...args, + }); + }, + async searchContacts(args = {}) { + return this._makeRequest({ + path: "/v2/requesters/search", + ...args, + }); + }, + async filterContacts(args = {}) { + return this._makeRequest({ + path: "/v2/requesters/filter", + ...args, + }); + }, + // Agent methods + async getAgents(args = {}) { + return this._makeRequest({ + path: "/v2/agents", + ...args, + }); + }, + // Group methods + async getGroups(args = {}) { + return this._makeRequest({ + path: "/v2/groups", + ...args, + }); + }, + // Ticket methods + async createTicket(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/v2/tickets", + ...args, + }); + }, + async getTicket({ + ticketId, ...args + }) { + return this._makeRequest({ + path: `/v2/tickets/${ticketId}`, + ...args, + }); + }, + async listTickets(args = {}) { + return this._makeRequest({ + path: "/v2/tickets", + ...args, + }); + }, + async updateTicket({ + ticketId, ...args + }) { + return this._makeRequest({ + method: "PUT", + path: `/v2/tickets/${ticketId}`, + ...args, + }); + }, + async searchTickets(args = {}) { + return this._makeRequest({ + path: "/v2/tickets/search", + ...args, + }); + }, + async filterTickets(args = {}) { + return this._makeRequest({ + path: "/v2/tickets/filter", + ...args, + }); + }, + async getTicketName(ticketId) { + const { ticket } = await this.getTicket({ ticketId }); + return ticket?.subject || ticketId; + }, + // Note methods + async createNote({ + ticketId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/v2/tickets/${ticketId}/notes`, + ...args, + }); + }, async getTickets(args = {}) { return this._makeRequest({ path: "/v2/tickets", diff --git a/components/freshservice/package.json b/components/freshservice/package.json index 728e72d3243f5..f0fd59701b21d 100644 --- a/components/freshservice/package.json +++ b/components/freshservice/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/freshservice", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Freshservice Components", "main": "freshservice.app.mjs", "keywords": [ @@ -14,6 +14,8 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3", + "async-retry": "^1.3.3", + "moment": "2.29.4" } } diff --git a/components/freshservice/sources/new-contact/new-contact.mjs b/components/freshservice/sources/new-contact/new-contact.mjs new file mode 100644 index 0000000000000..1b0856eeeb159 --- /dev/null +++ b/components/freshservice/sources/new-contact/new-contact.mjs @@ -0,0 +1,84 @@ +import moment from "moment"; +import freshservice from "../../freshservice.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + name: "New Contact", + version: "0.0.1", + key: "freshservice-new-contact", + description: "Emit new event for each created contact. [See documentation](https://api.freshservice.com/v2/#list_all_requesters)", + type: "source", + dedupe: "unique", + props: { + freshservice, + db: "$.service.db", + timer: { + type: "$.interface.timer", + static: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + emitEvent(data) { + this.$emit(data, { + id: data.id, + summary: `New contact: ${data.first_name} ${data.last_name || ""} (${data.email})`, + ts: Date.parse(data.created_at), + }); + }, + async *getContacts(params = {}) { + let page = 1; + let hasMore = true; + + while (hasMore) { + const { requesters } = await this.freshservice.filterContacts({ + params: { + ...params, + page, + per_page: 100, + }, + }); + + if (!requesters || requesters.length === 0) { + hasMore = false; + } else { + for (const contact of requesters) { + yield contact; + } + page++; + } + } + }, + async emitEvents() { + const lastDateChecked = await this.freshservice.getLastDateChecked(); + const formattedDate = moment(lastDateChecked).format("YYYY-MM-DDTHH:mm:ss[Z]"); + + const params = { + query: `"created_at:>'${formattedDate}'"`, + order_by: "created_at", + order_type: "asc", + }; + + let maxCreatedAt = lastDateChecked; + + for await (const contact of this.getContacts(params)) { + this.emitEvent(contact); + + if (contact.created_at > maxCreatedAt) { + maxCreatedAt = contact.created_at; + } + } + + await this.freshservice.setLastDateChecked(maxCreatedAt); + }, + }, + hooks: { + async deploy() { + await this.emitEvents(); + }, + }, + async run() { + await this.emitEvents(); + }, +}; \ No newline at end of file diff --git a/components/freshservice/sources/new-ticket/new-ticket.mjs b/components/freshservice/sources/new-ticket/new-ticket.mjs index c34f7e71676d8..88f76f7b72fed 100644 --- a/components/freshservice/sources/new-ticket/new-ticket.mjs +++ b/components/freshservice/sources/new-ticket/new-ticket.mjs @@ -1,15 +1,16 @@ -import app from "../../freshservice.app.mjs"; +import moment from "moment"; +import freshservice from "../../freshservice.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { name: "New Ticket", - version: "0.0.2", + version: "0.0.3", key: "freshservice-new-ticket", - description: "Emit new event for each created ticket. [See documentation](https://api.freshservice.com/#view_all_ticket)", + description: "Emit new event for each created ticket. [See documentation](https://api.freshservice.com/v2/#list_all_tickets)", type: "source", dedupe: "unique", props: { - app, + freshservice, db: "$.service.db", timer: { type: "$.interface.timer", @@ -22,19 +23,54 @@ export default { emitEvent(data) { this.$emit(data, { id: data.id, - summary: `New ticket with ID ${data.id}`, + summary: `New ticket: ${data.subject || data.id}`, ts: Date.parse(data.created_at), }); }, + async *getTickets(params = {}) { + let page = 1; + let hasMore = true; + + while (hasMore) { + const { tickets } = await this.freshservice.filterTickets({ + params: { + ...params, + page, + per_page: 100, + }, + }); + + if (!tickets || tickets.length === 0) { + hasMore = false; + } else { + for (const ticket of tickets) { + yield ticket; + } + page++; + } + } + }, async emitEvents() { - const { tickets: resources } = await this.app.getTickets({ - params: { - filter: "new_and_my_open", - order_type: "desc", - }, - }); + const lastDateChecked = await this.freshservice.getLastDateChecked(); + const formattedDate = moment(lastDateChecked).format("YYYY-MM-DDTHH:mm:ss[Z]"); + + const params = { + query: `"created_at:>'${formattedDate}'"`, + order_by: "created_at", + order_type: "asc", + }; + + let maxCreatedAt = lastDateChecked; + + for await (const ticket of this.getTickets(params)) { + this.emitEvent(ticket); + + if (ticket.created_at > maxCreatedAt) { + maxCreatedAt = ticket.created_at; + } + } - resources.reverse().forEach(this.emitEvent); + await this.freshservice.setLastDateChecked(maxCreatedAt); }, }, hooks: {