Skip to content

Commit

Permalink
feat(freshdesk): add Freshdesk users integration (#63)
Browse files Browse the repository at this point in the history
## Describe your changes
Add the following to Freshdesk integration


https://www.loom.com/share/b6d2bf3ce1c7430780a7094fd739a6a6?sid=4067bdca-418c-4979-b224-e49165f42891

### Syncs
- users

### Actions
- create-user
- delete-user

**NOTE**: (Watch the video for the report!) The freshdesk users sync
pagination system is not working. I have two hypothesis:
1. There might be a bug in nango's [*link async generator
function](https://github.com/NangoHQ/nango/blob/f3743a2104030f5737f9916125dcc1873f226440/packages/shared/lib/services/paginate.service.ts#L79C5-L116C6).
Also, I'm not sure how the current could possibly update the `page`
query parameter the next page number is extracted from the link based on
the following proxy configuration
```
   const proxyConfiguration: ProxyConfiguration = {
        endpoint: '/api/v2/contacts',
        retries: 10,
        paginate: {
// where does the "page" query parameter go?
            type: 'link',
            limit_name_in_request: 'per_page',
            link_rel_in_response_header: 'next', 
            limit: 1
        }
    };
```
  OR
2. Freshdesk list of contacts API response is not truly providing 

> The 'link' header in the response will hold the next page url if
exists. If you have reached the last page of objects, then the link
header will not be set.
https://developer.freshdesk.com/api/#pagination

## Issue ticket number and link

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [X] I added tests, otherwise the reason is:
- [X] External API requests have `retries`
- [X] Pagination is used where appropriate
- [X] The built in `nango.paginate` call is used instead of a `while
(true)` loop
- [ ] Third party requests are NOT parallelized (this can cause issues
with rate limits)
- [X] If a sync requires metadata the `nango.yaml` has `auto_start:
false`
- [X] If the sync is a `full` sync then `track_deletes: true` is set

---------

Co-authored-by: Khaliq <khaliqgant@gmail.com>
Co-authored-by: Khaliq <khaliq@nango.dev>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent 6f59edc commit 1be625c
Show file tree
Hide file tree
Showing 41 changed files with 1,305 additions and 35 deletions.
121 changes: 121 additions & 0 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1776,7 +1776,116 @@ integrations:
track_deletes: true
runs: every day
output: Article
contacts:
description: |
Fetches the list of contacts.
endpoint: GET /contacts
sync_type: incremental
runs: every day
output: Contact
tickets:
description: |
Fetches the freshdesk tickets
endpoint: GET /tickets
sync_type: incremental
runs: every day
output: Ticket
users:
description: |
Fetches the list of users
endpoint: GET /users
sync_type: full
track_deletes: true
runs: every day
output: User
actions:
create-contact:
description: Creates a user in FreshDesk
output: Contact
endpoint: POST /contacts
input: CreateContact
delete-contact:
description: Deletes a contact in FreshDesk
endpoint: DELETE /contacts
output: SuccessResponse
input: IdEntity
create-user:
description: Creates a user in FreshDesk
output: User
endpoint: POST /users
input: FreshdeskCreateUser
delete-user:
description: Deletes a user in FreshDesk
endpoint: DELETE /users
output: SuccessResponse
input: IdEntity
models:
IdEntity:
id: string
SuccessResponse:
success: boolean
CreateUser:
firstName: string
lastName: string
email: string
FreshdeskCreateUser:
firstName: string
lastName: string
email: string
ticket_scope?: number
ticketScope?: globalAccess | groupAccess | restrictedAccess
occasional?: boolean
signature?: string
skill_ids?: number[]
group_ids?: number[]
role_ids?: number[]
agent_type?: number
agentType?: support | field | collaborator
language?: string
time_zone?: string
focus_mode?: boolean
User:
id: string
email: string
firstName: string
lastName: string
Contact:
id: string
active: boolean
email: string
name: string
createdAt: string
updatedAt: string
companyId?: string | undefined
phone?: string | undefined | null
mobile?: string | undefined | null
jobTitle?: string | undefined | null
CreateContact:
name: string
email?: string
phone?: string
mobile?: string
twitter_id?:
type: string
unique: true
required: true
unique_external_id?:
type: string
unique: true
required: true
other_emails?: array
company_id?: number
view_all_tickets?: boolean
other_companies?: array
address?: string
avatar?: object
custom_fields?: object
description?: string
job_title?: string
language?: string
tags?: array
time_zone?: string
lookup_parameter?: string
Timestamps:
created_at: string
updated_at: string
Expand Down Expand Up @@ -1810,6 +1919,18 @@ integrations:
meta_title?: string | undefined
meta_description?: string | undefined
meta_keywords?: string | undefined
Ticket:
id: string
type: string
priority: number
request_id: number
response_id: number
source: number
subject: string
to_emails: string[] | null
created_at: string
updated_at: string
is_escalated: boolean
github:
syncs:
issues:
Expand Down
48 changes: 48 additions & 0 deletions integrations/freshdesk/actions/create-contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { NangoAction, ProxyConfiguration, Contact, CreateContact } from '../../models';
import { createContactSchema } from '../schema.zod.js';
import type { FreshdeskContact } from '../types';
import { toContact } from '../mappers/to-contact.js';

/**
* Creates a new user in Freshdesk by validating input data against a schema,
* sending a request to the Freshdesk API, logging any validation errors, and
* returning a common Nango Contact object
*
* Create user Freshdesk API docs: https://developer.freshdesk.com/api/#create_contact
*
*/
export default async function runAction(nango: NangoAction, input: CreateContact): Promise<Contact> {
const parsedInput = createContactSchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to create a contact: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}

throw new nango.ActionError({
message: 'Invalid input provided to create a contact'
});
}

const { email, phone, mobile } = parsedInput.data;

if (!email && !phone && !mobile) {
await nango.log('At least one of email, phone, or mobile must be provided.', { level: 'error' });

throw new nango.ActionError({
message: 'At least one of email, phone, or mobile must be provided.'
});
}

const config: ProxyConfiguration = {
endpoint: `/api/v2/contacts`,
data: parsedInput.data,
retries: 10
};

const response = await nango.post<FreshdeskContact>(config);

const { data: freshdeskContact } = response;

return toContact(freshdeskContact);
}
76 changes: 76 additions & 0 deletions integrations/freshdesk/actions/create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { NangoAction, ProxyConfiguration, User, FreshdeskCreateUser } from '../../models';
import { freshdeskCreateUserSchema } from '../schema.zod.js';
import type { FreshdeskAgent } from '../types';

export default async function runAction(nango: NangoAction, input: FreshdeskCreateUser): Promise<User> {
const parsedInput = freshdeskCreateUserSchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to create a user: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}

throw new nango.ActionError({
message: 'Invalid input provided to create a user'
});
}

const { firstName, lastName, ...userInput } = parsedInput.data;

userInput.ticket_scope = categorizeTicketScope(userInput.ticketScope || 'globalAccess');

if (userInput.agentType) {
userInput.agent_type = categorizeAgentType(userInput.agentType);
}

const config: ProxyConfiguration = {
// https://developer.freshdesk.com/api/#create_agent
endpoint: `/api/v2/agents`,
data: {
...userInput,
name: `${firstName} ${lastName}`
},
retries: 10
};

const response = await nango.post<FreshdeskAgent>(config);

const { data } = response;

const [firstNameOutput, lastNameOutput] = (data?.contact?.name ?? '').split(' ');

const user: User = {
id: data.id.toString(),
firstName: firstNameOutput || firstName,
lastName: lastNameOutput || lastName,
email: data.contact.email || ''
};

return user;
}

function categorizeTicketScope(ticketScope: string): number {
switch (ticketScope) {
case 'globalAccess':
return 1;
case 'groupAccess':
return 2;
case 'restrictedAccess':
return 3;
default:
return 1;
}
}

function categorizeAgentType(agentType: string): number {
switch (agentType) {
case 'support':
return 1;
case 'field':
return 2;
case 'collaborator':
return 3;
default:
return 1;
}
}
33 changes: 33 additions & 0 deletions integrations/freshdesk/actions/delete-contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { NangoAction, ProxyConfiguration, SuccessResponse, IdEntity } from '../../models';
import { idEntitySchema } from '../schema.zod.js';

/**
* Deletes a user in Freshdesk
*
* Delete user Freshdesk API docs: https://developer.freshdesk.com/api/#soft_delete_contact
*
*/
export default async function runAction(nango: NangoAction, input: IdEntity): Promise<SuccessResponse> {
const parsedInput = idEntitySchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to delete a user: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}

throw new nango.ActionError({
message: 'Invalid input provided to delete a user'
});
}

const config: ProxyConfiguration = {
endpoint: `/api/v2/contacts/${parsedInput.data.id}`,
retries: 10
};

await nango.delete(config);

return {
success: true
};
}
28 changes: 28 additions & 0 deletions integrations/freshdesk/actions/delete-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { NangoAction, ProxyConfiguration, SuccessResponse, IdEntity } from '../../models';
import { idEntitySchema } from '../schema.zod.js';

export default async function runAction(nango: NangoAction, input: IdEntity): Promise<SuccessResponse> {
const parsedInput = idEntitySchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to delete a user: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}

throw new nango.ActionError({
message: 'Invalid input provided to delete a user'
});
}

const config: ProxyConfiguration = {
// https://developer.freshdesk.com/api/#delete_agent
endpoint: `/api/v2/agents/${parsedInput.data.id}`,
retries: 10
};

await nango.delete(config);

return {
success: true
};
}
4 changes: 4 additions & 0 deletions integrations/freshdesk/fixtures/create-contact-iteration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"email": "john0${iteration}@doe.com",
"name": "John Doe"
}
4 changes: 4 additions & 0 deletions integrations/freshdesk/fixtures/create-contact.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"email": "john@doe.com",
"name": "John Doe"
}
5 changes: 5 additions & 0 deletions integrations/freshdesk/fixtures/create-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"email": "john@doe.com",
"firstName": "John",
"lastName": "Doe"
}
3 changes: 3 additions & 0 deletions integrations/freshdesk/fixtures/delete-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "501001204038"
}
17 changes: 17 additions & 0 deletions integrations/freshdesk/mappers/to-contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FreshdeskContact } from '../types';
import type { Contact } from '../../models';

export function toContact(contact: FreshdeskContact): Contact {
return {
id: contact.id.toString(),
name: contact.name,
active: contact.active,
email: contact.email,
companyId: contact.company_id?.toString(),
jobTitle: contact.job_title,
phone: contact.phone,
mobile: contact.mobile,
createdAt: contact.created_at,
updatedAt: contact.updated_at
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
13 changes: 13 additions & 0 deletions integrations/freshdesk/mocks/contacts/Contact/batchSave.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[
{
"id": "501001204038",
"name": "John Doe",
"active": false,
"email": "john1@doe.com",
"jobTitle": null,
"phone": null,
"mobile": null,
"createdAt": "2024-10-21T09:26:53Z",
"updatedAt": "2024-10-21T09:29:51Z"
}
]
5 changes: 5 additions & 0 deletions integrations/freshdesk/mocks/create-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"email": "john@doe.com",
"firstName": "John",
"lastName": "Doe"
}
6 changes: 6 additions & 0 deletions integrations/freshdesk/mocks/create-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "501001204047",
"firstName": "John",
"lastName": "Doe",
"email": "john@doe.com"
}
3 changes: 3 additions & 0 deletions integrations/freshdesk/mocks/delete-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "501001204038"
}
3 changes: 3 additions & 0 deletions integrations/freshdesk/mocks/delete-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"success": true
}
Loading

0 comments on commit 1be625c

Please sign in to comment.