Skip to content

Commit

Permalink
feat(calendly): Add users syncs/actions (#57)
Browse files Browse the repository at this point in the history
## Describe your changes
Add the following endpoints to the Calendly integration

### Syncs
- Users

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

**Note**: I wasn't able to run successful tests for create-user and
delete-user actions due to lack of permissions.

![Screenshot 2024-10-17 at 11 54
33 AM](https://github.com/user-attachments/assets/08145477-74c9-452b-98d6-ce0bf15b07d7)


## Issue ticket number and link
N/A

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] 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
- [x] Third party requests are NOT parallelized (this can cause issues
with rate limits)
- [ ] 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 17, 2024
1 parent 64d32bc commit 614f8ff
Show file tree
Hide file tree
Showing 20 changed files with 740 additions and 3 deletions.
33 changes: 33 additions & 0 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,40 @@ integrations:
endpoint: GET /event/invitees
output: EventInvitee
description: For all events (active and canceled) retrieve the event invitees
users:
runs: every day
description: Fetches a list of users from Calendly
output: User
track_deletes: true
sync_type: full
endpoint: GET /users
actions:
create-user:
description: Creates a user in Calendly
endpoint: POST /users
output: User
input: CreateUser
scopes:
- admin
delete-user:
description: Deletes a user in Calendly
endpoint: DELETE /users
output: SuccessResponse
input: IdEntity
scopes:
- admin
models:
IdEntity:
id: string
SuccessResponse:
success: boolean
User:
id: string
email: string
firstName: string
lastName: string
CreateUser:
email: string
Event:
id: string
uri: string
Expand Down
45 changes: 45 additions & 0 deletions integrations/calendly/actions/create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { NangoAction, ProxyConfiguration, User, CreateUser } from '../../models';
import { getOrganizationId } from '../helpers/get-organizationId.js';
import { createUserSchema } from '../schema.zod.js';
import type { OrganizationInvitation } from '../types';

/**
* Executes the create user action by validating input, constructing the request configuration,
* and making the Calendly API call to invitate (create) a new user to an organization.
*/
export default async function runAction(nango: NangoAction, input: CreateUser): Promise<User> {
const parsedInput = createUserSchema.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 organization = await getOrganizationId(nango);

const config: ProxyConfiguration = {
// https://developer.calendly.com/api-docs/094d15d2cd4ab-invite-user-to-organization
endpoint: `/organizations/${organization.id}/invitations`,
data: {
email: parsedInput.data.email
},
retries: 10
};

const response = await nango.post<{ resource: OrganizationInvitation }>(config);

const newUser = response.data.resource;
const user: User = {
id: newUser.uri.split('/').pop() ?? '',
firstName: '',
lastName: '',
email: newUser.email
};

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

/**
* Executes the delete user action by validating input, constructing the endpoint,
* and making the API call to Calendly to delete the user from an organization.
*/
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.calendly.com/api-docs/269e89d9f559f-remove-user-from-organization
endpoint: `/organization_memberships/${parsedInput.data.id}`,
retries: 10
};

await nango.delete(config);

return {
success: true
};
}
3 changes: 3 additions & 0 deletions integrations/calendly/fixtures/create-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"email": "john@doe.com"
}
3 changes: 3 additions & 0 deletions integrations/calendly/fixtures/delete-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "HBADPPGMUEGYL6RY"
}
42 changes: 42 additions & 0 deletions integrations/calendly/helpers/get-organizationId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { NangoAction, NangoSync } from '../../models';
import type { CalendlyCurrentUser } from '../types';

/**
* Retrieves the current organization id (uri and id) associated with the
* logged-in user from the Calendly API.
*/
export async function getOrganizationId(nango: NangoAction | NangoSync): Promise<{ uri: string; id: string }> {
const connection = await nango.getConnection();
let organizationBaseUri = connection.connection_config['organizationId'];

if (organizationBaseUri) return mapOrganizationId(organizationBaseUri);

await nango.log(`No organization id found in the connection config. Attempting to get it from fetching it from the logged-in user`);

const response = await nango.get<{ resource: CalendlyCurrentUser }>({
// https://developer.calendly.com/api-docs/005832c83aeae-get-current-user
endpoint: '/users/me',
retries: 10
});

if (!response.data) {
throw new nango.ActionError({ message: 'failed to get request info' });
}

organizationBaseUri = response.data.resource.current_organization;

if (!organizationBaseUri) {
throw new nango.ActionError({ message: 'failed to get organization id' });
}

await nango.log(`Got organization id`);

return mapOrganizationId(organizationBaseUri);
}

function mapOrganizationId(organizationBaseUri: string): { uri: string; id: string } {
return {
id: organizationBaseUri.split('/').pop() ?? '',
uri: organizationBaseUri
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"collection": [
{
"created_at": "2024-07-08T07:48:15.158893Z",
"organization": "https://api.calendly.com/organizations/fe1dd514-15c3-48de-a662-1c5f76ba5402",
"role": "owner",
"updated_at": "2024-07-08T07:48:15.158893Z",
"uri": "https://api.calendly.com/organization_memberships/885176bb-4e76-4cc9-9cfc-3e42b3463274",
"user": {
"avatar_url": null,
"created_at": "2024-07-08T07:48:15.090124Z",
"email": "barry@some-email.com",
"locale": "en",
"name": "Brian Beautiful",
"timezone": "Europe/Berlin",
"updated_at": "2024-10-09T15:35:58.139120Z",
"uri": "https://api.calendly.com/users/e46e2d95-7782-4f5f-ba84-188f9fc8961c",
"scheduling_url": "https://calendly.com/barry-nango",
"slug": "barry-nango"
}
},
{
"created_at": "2024-07-08T07:50:46.134244Z",
"organization": "https://api.calendly.com/organizations/fe1dd514-15c3-48de-a662-1c5f76ba5402",
"role": "user",
"updated_at": "2024-07-08T07:50:46.134244Z",
"uri": "https://api.calendly.com/organization_memberships/7cd308d1-3312-414f-adcb-89a2eb737a54",
"user": {
"avatar_url": null,
"created_at": "2023-05-12T06:26:32.342945Z",
"email": "john@some-email.com",
"locale": "en",
"name": "John Faked",
"timezone": "Asia/Jerusalem",
"updated_at": "2024-10-09T14:57:44.698577Z",
"uri": "https://api.calendly.com/users/abcdegggggg",
"scheduling_url": "https://calendly.com/john-doe-nango",
"slug": "john-doe-nango"
}
}
],
"pagination": {
"count": 2,
"next_page": null,
"next_page_token": null,
"previous_page": null,
"previous_page_token": null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"resource": {
"avatar_url": null,
"created_at": "2023-05-12T06:26:32.342945Z",
"current_organization": "https://api.calendly.com/organizations/fe1dd514-15c3-48de-a662-1c5f76ba5402",
"email": "john@some-email.com",
"locale": "en",
"name": "John Faked",
"resource_type": "User",
"scheduling_url": "https://calendly.com/john-doe-nango",
"slug": "john-doe-nango",
"timezone": "Asia/Jerusalem",
"updated_at": "2024-10-09T14:57:44.698577Z",
"uri": "https://api.calendly.com/users/abcdegggggg"
}
}
16 changes: 16 additions & 0 deletions integrations/calendly/mocks/nango/get/proxy/users/me/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"resource": {
"avatar_url": null,
"created_at": "2023-05-12T06:26:32.342945Z",
"current_organization": "https://api.calendly.com/organizations/fe1dd514-15c3-48de-a662-1c5f76ba5402",
"email": "john@some-email.com",
"locale": "en",
"name": "John Faked",
"resource_type": "User",
"scheduling_url": "https://calendly.com/john-doe-nango",
"slug": "john-doe-nango",
"timezone": "Asia/Jerusalem",
"updated_at": "2024-10-09T14:57:44.698577Z",
"uri": "https://api.calendly.com/users/abcdegggggg"
}
}
2 changes: 1 addition & 1 deletion integrations/calendly/mocks/nango/getConnection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"metadata": null,
"connection_config": {
"owner": "https://api.calendly.com/users/111"
"owner": "https://api.calendly.com/users/abcdegggggg"
}
}
1 change: 1 addition & 0 deletions integrations/calendly/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/calendly/mocks/users/User/batchSave.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"id": "e46e2d95-7782-4f5f-ba84-188f9fc8961c",
"email": "barry@some-email.com",
"firstName": "Brian",
"lastName": "Beautiful"
},
{
"id": "abcdegggggg",
"email": "john@some-email.com",
"firstName": "John",
"lastName": "Faked"
}
]
37 changes: 37 additions & 0 deletions integrations/calendly/nango.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,45 @@ integrations:
endpoint: GET /event/invitees
output: EventInvitee
description: For all events (active and canceled) retrieve the event invitees
users:
runs: every day
description: Fetches a list of users from Calendly
output: User
track_deletes: true
sync_type: full
endpoint: GET /users
actions:
create-user:
description: Creates a user in Calendly
endpoint: POST /users
output: User
input: CreateUser
scopes:
- admin
delete-user:
description: Deletes a user in Calendly
endpoint: DELETE /users
output: SuccessResponse
input: IdEntity
scopes:
- admin

models:
# Generic
IdEntity:
id: string
SuccessResponse:
success: boolean

# Users
User:
id: string
email: string
firstName: string
lastName: string
CreateUser:
email: string

Event:
id: string
uri: string
Expand Down
19 changes: 19 additions & 0 deletions integrations/calendly/schema.zod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
// Generated by ts-to-zod
import { z } from 'zod';

export const idEntitySchema = z.object({
id: z.string()
});

export const successResponseSchema = z.object({
success: z.boolean()
});

export const userSchema = z.object({
id: z.string(),
email: z.string(),
firstName: z.string(),
lastName: z.string()
});

export const createUserSchema = z.object({
email: z.string()
});

export const eventLocationSchema = z.object({
type: z.string(),
location: z.string().optional(),
Expand Down
Loading

0 comments on commit 614f8ff

Please sign in to comment.