Skip to content

Commit

Permalink
feat(harvest): add Harvest integration (#71)
Browse files Browse the repository at this point in the history
## Describe your changes
Add harvest integrations with the following endpoints:
https://www.loom.com/share/b5a9580f7ca14e7cb5bd6f12d3337e43

### Syncs
- users

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

## Issue ticket number and link
N/A

## 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 <[email protected]>
  • Loading branch information
arealesramirez and khaliqgant authored Oct 25, 2024
1 parent bbc2526 commit 9034419
Show file tree
Hide file tree
Showing 23 changed files with 900 additions and 299 deletions.
60 changes: 60 additions & 0 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3187,6 +3187,66 @@ integrations:
track_editor_paste: boolean
show_copy_paste_prompt: boolean
ide_config: string
harvest:
syncs:
users:
description: |
Fetches the list of users in Harvest
endpoint: GET /users
sync_type: full
track_deletes: true
runs: every day
output: User
scopes:
- administrator
- manager
actions:
create-user:
description: Creates a user in Harvest
output: User
endpoint: POST /users
input: HarvestCreateUser
scopes:
- administrator
- manager
delete-user:
description: Deletes a user in Harvest
endpoint: DELETE /users
output: SuccessResponse
input: IdEntity
scopes:
- administrator
models:
IdEntity:
id: string
SuccessResponse:
success: boolean
CreateUser:
first_name: string
last_name: string
email: string
HarvestCreateUser:
first_name: string
last_name: string
email: string
timezone?: string
has_access_to_all_future_projects?: boolean
is_contractor?: boolean
is_active?: boolean
weekly_capacity?: integer
default_hourly_rate?: decimal
cost_rate?: decimal
roles?: string[]
access_roles?: >-
administrator | manager | member | project_creator |
billable_rates_manager | managed_projects_invoice_drafter |
managed_projects_invoice_manager | client_and_task_manager |
time_and_expenses_manager | estimates_manager
User:
id: string
email: string
firstName: string
lastName: string
hibob-service-user:
syncs:
employees:
Expand Down
47 changes: 47 additions & 0 deletions integrations/harvest/actions/create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { NangoAction, ProxyConfiguration, User, HarvestCreateUser } from '../../models';
import { toUser } from '../mappers/to-user.js';
import { harvestCreateUserSchema } from '../schema.zod.js';
import type { HarvestUser } from '../types';

/**
* Creates a Haverst user.
*
* This function validates the input against the defined schema and constructs a request
* to the Harvest API to create a new user contact. If the input is invalid, it logs the
* errors and throws an ActionError.
*
* @param {NangoAction} nango - The Nango action context, used for logging and making API requests.
* @param {HaverstCreateUser} input - The input data for creating a user contact
*
* @returns {Promise<User>} - A promise that resolves to the created User object.
*
* @throws {nango.ActionError} - Throws an error if the input validation fails.
*
* For detailed endpoint documentation, refer to:
* https://help.getharvest.com/api-v2/users-api/users/users/#create-a-user
*/
export default async function runAction(nango: NangoAction, input: HarvestCreateUser): Promise<User> {
const parsedInput = harvestCreateUserSchema.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 config: ProxyConfiguration = {
// https://help.getharvest.com/api-v2/users-api/users/users/#create-a-user
endpoint: `/v2/users`,
data: parsedInput.data,
retries: 10
};

const response = await nango.post<HarvestUser>(config);
const { data } = response;

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

/**
* Deletes a Haverst user.
*
* This function validates the input against the defined schema and constructs a request
* to the Harvest API to delete a user contact by their ID. If the input is invalid,
* it logs the errors and throws an ActionError.
*
* @param {NangoAction} nango - The Nango action context, used for logging and making API requests.
* @param {IdEntity} input - The input data containing the ID of the user contact to be deleted
*
* @returns {Promise<SuccessResponse>} - A promise that resolves to a SuccessResponse object indicating the result of the deletion.
*
* @throws {nango.ActionError} - Throws an error if the input validation fails.
*
* For detailed endpoint documentation, refer to:
* https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/deletecontact
*/
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://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/deletecontact
endpoint: `/v2/users/${parsedInput.data.id}`,
retries: 10
};

await nango.delete(config);

return {
success: true
};
}
5 changes: 5 additions & 0 deletions integrations/harvest/fixtures/create-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe"
}
3 changes: 3 additions & 0 deletions integrations/harvest/fixtures/delete-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "5083917"
}
17 changes: 17 additions & 0 deletions integrations/harvest/mappers/to-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { User } from '../../models';
import type { HarvestUser } from '../types';

/**
* Maps an Harvest API user object to a Nango User object.
*
* @param contact The raw user object from the Harvest API.
* @returns Mapped Nango User object with essential properties.
*/
export function toUser(harvestUser: HarvestUser): User {
return {
id: harvestUser.id.toString(),
email: harvestUser.email,
firstName: harvestUser.first_name,
lastName: harvestUser.last_name
};
}
5 changes: 5 additions & 0 deletions integrations/harvest/mocks/create-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe"
}
6 changes: 6 additions & 0 deletions integrations/harvest/mocks/create-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "5083917",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe"
}
3 changes: 3 additions & 0 deletions integrations/harvest/mocks/delete-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "5083917"
}
3 changes: 3 additions & 0 deletions integrations/harvest/mocks/delete-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"success": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"id": 5083917,
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"telephone": "",
"timezone": "Central Time (US & Canada)",
"weekly_capacity": 126000,
"has_access_to_all_future_projects": false,
"is_contractor": false,
"is_active": true,
"calendar_integration_enabled": false,
"calendar_integration_source": null,
"created_at": "2024-10-24T11:55:46Z",
"updated_at": "2024-10-24T12:11:53Z",
"can_create_projects": false,
"default_hourly_rate": null,
"cost_rate": null,
"roles": [],
"access_roles": [],
"permissions_claims": ["expenses:read:own", "expenses:write:own", "timers:read:own", "timers:write:own"],
"avatar_url": "https://d3s3969qhosaug.cloudfront.net/v2/default-avatars/4a44.png"
}
89 changes: 89 additions & 0 deletions integrations/harvest/mocks/nango/get/proxy/v2/users/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"users": [
{
"id": 5085064,
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"telephone": "",
"timezone": "Jerusalem",
"weekly_capacity": 126000,
"has_access_to_all_future_projects": false,
"is_contractor": false,
"is_active": true,
"calendar_integration_enabled": false,
"calendar_integration_source": null,
"created_at": "2024-10-25T17:01:08Z",
"updated_at": "2024-10-25T17:01:08Z",
"can_create_projects": false,
"default_hourly_rate": null,
"cost_rate": null,
"roles": [],
"access_roles": ["member"],
"permissions_claims": ["expenses:read:own", "expenses:write:own", "timers:read:own", "timers:write:own"],
"avatar_url": "https://d3s3969qhosaug.cloudfront.net/v2/default-avatars/4a44.png"
},
{
"id": 5085050,
"first_name": "Johnny",
"last_name": "Doeseph",
"email": "[email protected]",
"telephone": "",
"timezone": "Jerusalem",
"weekly_capacity": 126000,
"has_access_to_all_future_projects": false,
"is_contractor": false,
"is_active": true,
"calendar_integration_enabled": false,
"calendar_integration_source": null,
"created_at": "2024-10-25T16:42:52Z",
"updated_at": "2024-10-25T16:43:23Z",
"can_create_projects": true,
"default_hourly_rate": null,
"cost_rate": null,
"roles": [],
"access_roles": ["administrator"],
"permissions_claims": [
"billable_rates:read:all",
"billable_rates:write:all",
"billing:read:own",
"billing:write:own",
"clients:read:all",
"clients:write:all",
"company:read:own",
"company:write:own",
"cost_rates:read:all",
"cost_rates:write:all",
"estimates:read:all",
"estimates:write:all",
"expenses:read:all",
"expenses:write:all",
"invoices:read:all",
"invoices:write:all",
"projects:read:all",
"projects:write:all",
"saved_reports:read:inactive",
"saved_reports:write:inactive",
"tasks:read:all",
"tasks:write:all",
"timers:read:all",
"timers:write:all",
"users:read:all",
"users:write:all"
],
"avatar_url": "https://d3s3969qhosaug.cloudfront.net/v2/default-avatars/4b47.png"
}
],
"per_page": 2000,
"total_pages": 1,
"total_entries": 2,
"next_page": null,
"previous_page": null,
"page": 1,
"links": {
"first": "https://api.harvestapp.com/v2/users?is_active=true&page=1&per_page=2000&ref=first",
"next": null,
"previous": null,
"last": "https://api.harvestapp.com/v2/users?is_active=true&page=1&per_page=2000&ref=last"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"id": 5083917,
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"telephone": "",
"timezone": "Central Time (US & Canada)",
"weekly_capacity": 126000,
"has_access_to_all_future_projects": false,
"is_contractor": false,
"is_active": true,
"calendar_integration_enabled": false,
"calendar_integration_source": null,
"created_at": "2024-10-24T11:55:46Z",
"updated_at": "2024-10-24T11:55:46Z",
"can_create_projects": false,
"default_hourly_rate": null,
"cost_rate": null,
"roles": [],
"access_roles": ["member"],
"permissions_claims": ["expenses:read:own", "expenses:write:own", "timers:read:own", "timers:write:own"],
"avatar_url": "https://d3s3969qhosaug.cloudfront.net/v2/default-avatars/4a44.png"
}
1 change: 1 addition & 0 deletions integrations/harvest/mocks/users/User/batchDelete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
14 changes: 14 additions & 0 deletions integrations/harvest/mocks/users/User/batchSave.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"id": "5085064",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe"
},
{
"id": "5085050",
"email": "[email protected]",
"firstName": "Johnny",
"lastName": "Doeseph"
}
]
Loading

0 comments on commit 9034419

Please sign in to comment.