diff --git a/flows.yaml b/flows.yaml index 993b1cfc3..db22cc2fd 100644 --- a/flows.yaml +++ b/flows.yaml @@ -3605,6 +3605,16 @@ integrations: input: IdEntity endpoint: GET /single-article output: Article + create-user: + description: Creates a user in Intercom + input: IntercomCreateUser + endpoint: POST /users + output: User + delete-user: + description: Deletes a user in Intercom + endpoint: DELETE /users + output: SuccessResponse + input: IdEntity syncs: conversations: runs: every 6 hours @@ -3636,9 +3646,18 @@ integrations: track_deletes: true version: 1.0.0 endpoint: GET /articles + users: + runs: every 6 hours + description: | + Fetches a list of users from Intercom + output: User + sync_type: incremental + endpoint: GET /users models: IdEntity: id: string + SuccessResponse: + success: boolean Contact: id: string workspace_id: string @@ -3741,6 +3760,26 @@ integrations: pt-BR: ArticleContent | null zh-CN: ArticleContent | null zh-TW: ArticleContent | null + User: + id: string + email: string + firstName: string + lastName: string + CreateUser: + firstName: string + lastName: string + email: string + IntercomCreateUser: + firstName: string + lastName: string + email: string + external_id?: string + phone?: string + avatar?: string + signed_up_at?: number + last_seen_at?: number + owner_id?: string + unsubscribed_from_emails?: boolean jira: syncs: issues: diff --git a/integrations/intercom/actions/create-user.ts b/integrations/intercom/actions/create-user.ts new file mode 100644 index 000000000..a693a643a --- /dev/null +++ b/integrations/intercom/actions/create-user.ts @@ -0,0 +1,52 @@ +import type { NangoAction, ProxyConfiguration, User, IntercomCreateUser } from '../../models'; +import { toUser } from '../mappers/to-user.js'; +import { intercomCreateUserSchema } from '../schema.zod.js'; +import type { IntercomContact } from '../types'; + +/** + * Creates an Intercom user contact. + * + * This function validates the input against the defined schema and constructs a request + * to the Intercom 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 {IntercomCreateUser} input - The input data for creating a user contact + * + * @returns {Promise} - 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://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/createcontact + */ +export default async function runAction(nango: NangoAction, input: IntercomCreateUser): Promise { + const parsedInput = intercomCreateUserSchema.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; + + const config: ProxyConfiguration = { + // https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/createcontact + endpoint: `/contacts`, + data: { + ...userInput, + role: 'user', + name: `${firstName} ${lastName}` + }, + retries: 10 + }; + + const response = await nango.post(config); + + return toUser(response.data); +} diff --git a/integrations/intercom/actions/delete-user.ts b/integrations/intercom/actions/delete-user.ts new file mode 100644 index 000000000..c51045dd6 --- /dev/null +++ b/integrations/intercom/actions/delete-user.ts @@ -0,0 +1,46 @@ +import type { NangoAction, ProxyConfiguration, SuccessResponse, IdEntity } from '../../models'; +import { idEntitySchema } from '../schema.zod.js'; +import type { IntercomDeleteContactResponse } from '../types'; + +/** + * Deletes an Intercom user contact. + * + * This function validates the input against the defined schema and constructs a request + * to the Intercom 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} - 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 { + 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: `/contacts/${parsedInput.data.id}`, + retries: 10 + }; + + const response = await nango.delete(config); + + return { + success: response.data.deleted + }; +} diff --git a/integrations/intercom/fixtures/create-user.json b/integrations/intercom/fixtures/create-user.json new file mode 100644 index 000000000..c0fd395d2 --- /dev/null +++ b/integrations/intercom/fixtures/create-user.json @@ -0,0 +1,5 @@ +{ + "email": "john@doe.com", + "firstName": "John", + "lastName": "Doe" +} diff --git a/integrations/intercom/fixtures/delete-user.json b/integrations/intercom/fixtures/delete-user.json new file mode 100644 index 000000000..e76932589 --- /dev/null +++ b/integrations/intercom/fixtures/delete-user.json @@ -0,0 +1,3 @@ +{ + "id": "671683207d3fbeb337586673" +} diff --git a/integrations/intercom/mappers/to-user.ts b/integrations/intercom/mappers/to-user.ts new file mode 100644 index 000000000..516a5afc3 --- /dev/null +++ b/integrations/intercom/mappers/to-user.ts @@ -0,0 +1,19 @@ +import type { User } from '../../models'; +import type { IntercomContact } from '../types'; + +/** + * Maps an Intercom API contact object to a Nango User object. + * + * @param contact The raw contact object from the Intercom API. + * @returns Mapped User object with essential properties. + */ +export function toUser(contact: IntercomContact): User { + const [firstName = '', lastName = ''] = (contact?.name ?? '').split(' '); + + return { + id: contact.id, + email: contact.email, + firstName, + lastName + }; +} diff --git a/integrations/intercom/mocks/create-user/input.json b/integrations/intercom/mocks/create-user/input.json new file mode 100644 index 000000000..c0fd395d2 --- /dev/null +++ b/integrations/intercom/mocks/create-user/input.json @@ -0,0 +1,5 @@ +{ + "email": "john@doe.com", + "firstName": "John", + "lastName": "Doe" +} diff --git a/integrations/intercom/mocks/create-user/output.json b/integrations/intercom/mocks/create-user/output.json new file mode 100644 index 000000000..5f92cdd31 --- /dev/null +++ b/integrations/intercom/mocks/create-user/output.json @@ -0,0 +1,6 @@ +{ + "id": "671683207d3fbeb337586673", + "email": "john@doe.com", + "firstName": "John", + "lastName": "Doe" +} diff --git a/integrations/intercom/mocks/delete-user/input.json b/integrations/intercom/mocks/delete-user/input.json new file mode 100644 index 000000000..e76932589 --- /dev/null +++ b/integrations/intercom/mocks/delete-user/input.json @@ -0,0 +1,3 @@ +{ + "id": "671683207d3fbeb337586673" +} diff --git a/integrations/intercom/mocks/delete-user/output.json b/integrations/intercom/mocks/delete-user/output.json new file mode 100644 index 000000000..33c0c847e --- /dev/null +++ b/integrations/intercom/mocks/delete-user/output.json @@ -0,0 +1,3 @@ +{ + "success": true +} diff --git a/integrations/intercom/mocks/nango/delete/proxy/contacts/671683207d3fbeb337586673/delete-user.json b/integrations/intercom/mocks/nango/delete/proxy/contacts/671683207d3fbeb337586673/delete-user.json new file mode 100644 index 000000000..ccfe2563a --- /dev/null +++ b/integrations/intercom/mocks/nango/delete/proxy/contacts/671683207d3fbeb337586673/delete-user.json @@ -0,0 +1,6 @@ +{ + "id": "671683207d3fbeb337586673", + "external_id": null, + "type": "contact", + "deleted": true +} diff --git a/integrations/intercom/mocks/nango/post/proxy/contacts/create-user.json b/integrations/intercom/mocks/nango/post/proxy/contacts/create-user.json new file mode 100644 index 000000000..7db4337ab --- /dev/null +++ b/integrations/intercom/mocks/nango/post/proxy/contacts/create-user.json @@ -0,0 +1,96 @@ +{ + "type": "contact", + "id": "671683207d3fbeb337586673", + "workspace_id": "s0lour9i", + "external_id": null, + "role": "user", + "email": "john@doe.com", + "phone": null, + "name": "John Doe", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1729528609, + "updated_at": 1729528609, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/671683207d3fbeb337586673/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/671683207d3fbeb337586673/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/671683207d3fbeb337586673/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/671683207d3fbeb337586673/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/671683207d3fbeb337586673/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false +} diff --git a/integrations/intercom/mocks/nango/post/proxy/contacts/search/users.json b/integrations/intercom/mocks/nango/post/proxy/contacts/search/users.json new file mode 100644 index 000000000..fb747ffa4 --- /dev/null +++ b/integrations/intercom/mocks/nango/post/proxy/contacts/search/users.json @@ -0,0 +1,690 @@ +{ + "type": "list", + "data": [ + { + "type": "contact", + "id": "66f7ee5ea9e2c41564b4c362", + "workspace_id": "s0lour9i", + "external_id": "3892ec77-2638-4343-a883-54c1ead2e35e", + "role": "user", + "email": "example.user@projectmap.com", + "phone": null, + "name": "Example User", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1727524446, + "updated_at": 1727737986, + "signed_up_at": 1727524446, + "last_seen_at": 1727524446, + "last_replied_at": 1727524463, + "last_contacted_at": 1727737986, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": "Ireland", + "region": "Leinster", + "city": "Dublin", + "country_code": "IRL", + "continent_code": "EU" + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/66f7ee5ea9e2c41564b4c362/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/66f7ee5ea9e2c41564b4c362/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [ + { + "id": "66f7ee5ea9e2c41564b4c363", + "type": "company", + "url": "/companies/66f7ee5ea9e2c41564b4c363" + } + ], + "url": "/contacts/66f7ee5ea9e2c41564b4c362/companies", + "total_count": 1, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66f7ee5ea9e2c41564b4c362/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66f7ee5ea9e2c41564b4c362/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + }, + { + "type": "contact", + "id": "66fadfecea421c7b5057f6f0", + "workspace_id": "s0lour9i", + "external_id": "1234566", + "role": "user", + "email": "john.doe@example.com", + "phone": null, + "name": "John Doe", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1727717356, + "updated_at": 1727717356, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/66fadfecea421c7b5057f6f0/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/66fadfecea421c7b5057f6f0/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/66fadfecea421c7b5057f6f0/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fadfecea421c7b5057f6f0/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fadfecea421c7b5057f6f0/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + }, + { + "type": "contact", + "id": "66fae008c39645865b3c0443", + "workspace_id": "s0lour9i", + "external_id": "23477", + "role": "user", + "email": "james@test.com", + "phone": null, + "name": "James Test", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1727717384, + "updated_at": 1727717384, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/66fae008c39645865b3c0443/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/66fae008c39645865b3c0443/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/66fae008c39645865b3c0443/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fae008c39645865b3c0443/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fae008c39645865b3c0443/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + }, + { + "type": "contact", + "id": "66fae01ac4f9bc7fb131a4a6", + "workspace_id": "s0lour9i", + "external_id": "894343", + "role": "user", + "email": "test@test.com", + "phone": null, + "name": null, + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1727717402, + "updated_at": 1727717402, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/66fae01ac4f9bc7fb131a4a6/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/66fae01ac4f9bc7fb131a4a6/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/66fae01ac4f9bc7fb131a4a6/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fae01ac4f9bc7fb131a4a6/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fae01ac4f9bc7fb131a4a6/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + }, + { + "type": "contact", + "id": "66fae05c6a8ee5104e5c6ee1", + "workspace_id": "s0lour9i", + "external_id": "324343", + "role": "user", + "email": "phil@inter.com", + "phone": null, + "name": "Phil Intercom", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1727717468, + "updated_at": 1727717468, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/66fae05c6a8ee5104e5c6ee1/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/66fae05c6a8ee5104e5c6ee1/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/66fae05c6a8ee5104e5c6ee1/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fae05c6a8ee5104e5c6ee1/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/66fae05c6a8ee5104e5c6ee1/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + }, + { + "type": "contact", + "id": "671682a3ff15ee50ba8c09f9", + "workspace_id": "s0lour9i", + "external_id": null, + "role": "user", + "email": "test1@test.com", + "phone": null, + "name": "tfirst last", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1729528483, + "updated_at": 1729528483, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/671682a3ff15ee50ba8c09f9/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/671682a3ff15ee50ba8c09f9/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/671682a3ff15ee50ba8c09f9/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/671682a3ff15ee50ba8c09f9/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/671682a3ff15ee50ba8c09f9/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + }, + { + "type": "contact", + "id": "67168a017aecdafcf9fa5cd0", + "workspace_id": "s0lour9i", + "external_id": null, + "role": "user", + "email": "john@doe.com", + "phone": null, + "name": "John Doe", + "avatar": null, + "owner_id": null, + "social_profiles": { + "type": "list", + "data": [] + }, + "has_hard_bounced": false, + "marked_email_as_spam": false, + "unsubscribed_from_emails": false, + "created_at": 1729530369, + "updated_at": 1729530369, + "signed_up_at": null, + "last_seen_at": null, + "last_replied_at": null, + "last_contacted_at": null, + "last_email_opened_at": null, + "last_email_clicked_at": null, + "language_override": null, + "browser": null, + "browser_version": null, + "browser_language": null, + "os": null, + "location": { + "type": "location", + "country": null, + "region": null, + "city": null, + "country_code": null, + "continent_code": null + }, + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_os_version": null, + "android_sdk_version": null, + "android_last_seen_at": null, + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_os_version": null, + "ios_sdk_version": null, + "ios_last_seen_at": null, + "custom_attributes": {}, + "tags": { + "type": "list", + "data": [], + "url": "/contacts/67168a017aecdafcf9fa5cd0/tags", + "total_count": 0, + "has_more": false + }, + "notes": { + "type": "list", + "data": [], + "url": "/contacts/67168a017aecdafcf9fa5cd0/notes", + "total_count": 0, + "has_more": false + }, + "companies": { + "type": "list", + "data": [], + "url": "/contacts/67168a017aecdafcf9fa5cd0/companies", + "total_count": 0, + "has_more": false + }, + "opted_out_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/67168a017aecdafcf9fa5cd0/subscriptions", + "total_count": 0, + "has_more": false + }, + "opted_in_subscription_types": { + "type": "list", + "data": [], + "url": "/contacts/67168a017aecdafcf9fa5cd0/subscriptions", + "total_count": 0, + "has_more": false + }, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "referrer": null, + "sms_consent": false, + "unsubscribed_from_sms": false + } + ], + "total_count": 7, + "pages": { + "type": "pages", + "page": 1, + "per_page": 50, + "total_pages": 1 + } +} diff --git a/integrations/intercom/mocks/users/User/batchDelete.json b/integrations/intercom/mocks/users/User/batchDelete.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/integrations/intercom/mocks/users/User/batchDelete.json @@ -0,0 +1 @@ +[] diff --git a/integrations/intercom/mocks/users/User/batchSave.json b/integrations/intercom/mocks/users/User/batchSave.json new file mode 100644 index 000000000..97ee994e5 --- /dev/null +++ b/integrations/intercom/mocks/users/User/batchSave.json @@ -0,0 +1,44 @@ +[ + { + "id": "66f7ee5ea9e2c41564b4c362", + "email": "example.user@projectmap.com", + "firstName": "Example", + "lastName": "User" + }, + { + "id": "66fadfecea421c7b5057f6f0", + "email": "john.doe@example.com", + "firstName": "John", + "lastName": "Doe" + }, + { + "id": "66fae008c39645865b3c0443", + "email": "james@test.com", + "firstName": "James", + "lastName": "Test" + }, + { + "id": "66fae01ac4f9bc7fb131a4a6", + "email": "test@test.com", + "firstName": "", + "lastName": "" + }, + { + "id": "66fae05c6a8ee5104e5c6ee1", + "email": "phil@inter.com", + "firstName": "Phil", + "lastName": "Intercom" + }, + { + "id": "671682a3ff15ee50ba8c09f9", + "email": "test1@test.com", + "firstName": "tfirst", + "lastName": "last" + }, + { + "id": "67168a017aecdafcf9fa5cd0", + "email": "john@doe.com", + "firstName": "John", + "lastName": "Doe" + } +] diff --git a/integrations/intercom/nango.yaml b/integrations/intercom/nango.yaml index 266e8d526..4f6f01854 100644 --- a/integrations/intercom/nango.yaml +++ b/integrations/intercom/nango.yaml @@ -6,6 +6,16 @@ integrations: input: IdEntity endpoint: GET /single-article output: Article + create-user: + description: Creates a user in Intercom + input: IntercomCreateUser + endpoint: POST /users + output: User + delete-user: + description: Deletes a user in Intercom + endpoint: DELETE /users + output: SuccessResponse + input: IdEntity syncs: conversations: runs: every 6 hours @@ -37,9 +47,18 @@ integrations: track_deletes: true version: 1.0.0 endpoint: GET /articles + users: + runs: every 6 hours + description: | + Fetches a list of users from Intercom + output: User + sync_type: incremental + endpoint: GET /users models: IdEntity: id: string + SuccessResponse: + success: boolean Contact: id: string workspace_id: string @@ -142,3 +161,23 @@ models: pt-BR: ArticleContent | null zh-CN: ArticleContent | null zh-TW: ArticleContent | null + + # Users + User: + id: string + email: string + firstName: string + lastName: string + CreateUser: + firstName: string + lastName: string + email: string + IntercomCreateUser: + __extends: CreateUser + external_id?: string + phone?: string + avatar?: string + signed_up_at?: number + last_seen_at?: number + owner_id?: string + unsubscribed_from_emails?: boolean diff --git a/integrations/intercom/schema.zod.ts b/integrations/intercom/schema.zod.ts index d784c89bf..0b5a05e2a 100644 --- a/integrations/intercom/schema.zod.ts +++ b/integrations/intercom/schema.zod.ts @@ -1,6 +1,14 @@ // 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 contactSchema = z.object({ id: z.string(), workspace_id: z.string(), @@ -118,3 +126,29 @@ export const articleSchema = z.object({ default_locale: z.union([z.string(), z.undefined()]).optional(), translated_content: z.union([translatedContentSchema, z.undefined()]).optional().nullable() }); + +export const userSchema = z.object({ + id: z.string(), + email: z.string(), + firstName: z.string(), + lastName: z.string() +}); + +export const createUserSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string() +}); + +export const intercomCreateUserSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string(), + external_id: z.string().optional(), + phone: z.string().optional(), + avatar: z.string().optional(), + signed_up_at: z.number().optional(), + last_seen_at: z.number().optional(), + owner_id: z.string().optional(), + unsubscribed_from_emails: z.boolean().optional() +}); diff --git a/integrations/intercom/syncs/contacts.ts b/integrations/intercom/syncs/contacts.ts index bc3a408df..e74db38ee 100644 --- a/integrations/intercom/syncs/contacts.ts +++ b/integrations/intercom/syncs/contacts.ts @@ -32,6 +32,21 @@ export default async function fetchData(nango: NangoSync): Promise { retries: 10 }; + if (nango.lastSyncDate) { + config.data = { + query: { + operator: 'AND', + value: [ + { + field: 'updated_at', + operator: '>', + value: Math.floor(nango.lastSyncDate.getTime() / 1000) + } + ] + } + }; + } + for await (const contacts of nango.paginate(config)) { const mappedContacts = contacts.map((contact: IntercomContact) => toContact(contact)); await nango.batchSave(mappedContacts, 'Contact'); diff --git a/integrations/intercom/syncs/users.ts b/integrations/intercom/syncs/users.ts new file mode 100644 index 000000000..40865395d --- /dev/null +++ b/integrations/intercom/syncs/users.ts @@ -0,0 +1,61 @@ +import type { NangoSync, ProxyConfiguration, User } from '../../models'; +import { toUser } from '../mappers/to-user.js'; +import type { IntercomContact } from '../types'; + +/** + * Fetches Intercom user contacts, maps them to Nango User objects, + * and saves the processed contacts using NangoSync. + * + * This function handles pagination and ensures that all contacts are fetched, + * transformed, and stored. + * + * For endpoint documentation, refer to: + * https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/listcontacts + * + * @param nango An instance of NangoSync for synchronization tasks. + * @returns Promise that resolves when all users are fetched and saved. + */ +export default async function fetchData(nango: NangoSync): Promise { + const queryValue: { field: string; operator: string; value: number | string }[] = [ + { + field: 'role', + operator: '=', + value: 'user' + } + ]; + + if (nango.lastSyncDate) { + queryValue.push({ + field: 'updated_at', + operator: '>', + value: Math.floor(nango.lastSyncDate.getTime() / 1000) + }); + } + + const config: ProxyConfiguration = { + // https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/listcontacts + endpoint: '/contacts/search', + paginate: { + type: 'cursor', + cursor_path_in_response: 'pages.next.starting_after', + limit_name_in_request: 'per_page', + cursor_name_in_request: 'starting_after', + response_path: 'data', + limit: 150 + }, + data: { + query: { + operator: 'AND', + value: queryValue + } + }, + method: 'POST', + retries: 10 + }; + + for await (const contacts of nango.paginate(config)) { + const users = contacts.map(toUser); + + await nango.batchSave(users, 'User'); + } +} diff --git a/integrations/intercom/tests/intercom-create-user.test.ts b/integrations/intercom/tests/intercom-create-user.test.ts new file mode 100644 index 000000000..92df80bfe --- /dev/null +++ b/integrations/intercom/tests/intercom-create-user.test.ts @@ -0,0 +1,19 @@ +import { vi, expect, it, describe } from "vitest"; + +import runAction from "../actions/create-user.js"; + +describe("intercom create-user tests", () => { + const nangoMock = new global.vitest.NangoActionMock({ + dirname: __dirname, + name: "create-user", + Model: "User" + }); + + it('should output the action output that is expected', async () => { + const input = await nangoMock.getInput(); + const response = await runAction(nangoMock, input); + const output = await nangoMock.getOutput(); + + expect(response).toEqual(output); + }); +}); diff --git a/integrations/intercom/tests/intercom-delete-user.test.ts b/integrations/intercom/tests/intercom-delete-user.test.ts new file mode 100644 index 000000000..0bfc32299 --- /dev/null +++ b/integrations/intercom/tests/intercom-delete-user.test.ts @@ -0,0 +1,19 @@ +import { vi, expect, it, describe } from "vitest"; + +import runAction from "../actions/delete-user.js"; + +describe("intercom delete-user tests", () => { + const nangoMock = new global.vitest.NangoActionMock({ + dirname: __dirname, + name: "delete-user", + Model: "SuccessResponse" + }); + + it('should output the action output that is expected', async () => { + const input = await nangoMock.getInput(); + const response = await runAction(nangoMock, input); + const output = await nangoMock.getOutput(); + + expect(response).toEqual(output); + }); +}); diff --git a/integrations/intercom/tests/intercom-users.test.ts b/integrations/intercom/tests/intercom-users.test.ts new file mode 100644 index 000000000..4ccdd9057 --- /dev/null +++ b/integrations/intercom/tests/intercom-users.test.ts @@ -0,0 +1,53 @@ +import { vi, expect, it, describe } from "vitest"; + +import fetchData from "../syncs/users.js"; + +describe("intercom users tests", () => { + const nangoMock = new global.vitest.NangoSyncMock({ + dirname: __dirname, + name: "users", + Model: "User" + }); + + const models = "User".split(','); + const batchSaveSpy = vi.spyOn(nangoMock, 'batchSave'); + + it("should get, map correctly the data and batchSave the result", async () => { + await fetchData(nangoMock); + + for (const model of models) { + const batchSaveData = await nangoMock.getBatchSaveData(model); + + const totalCalls = batchSaveSpy.mock.calls.length; + + if (totalCalls > models.length) { + const splitSize = Math.ceil(batchSaveData.length / totalCalls); + + const splitBatchSaveData = []; + for (let i = 0; i < totalCalls; i++) { + const chunk = batchSaveData.slice(i * splitSize, (i + 1) * splitSize); + splitBatchSaveData.push(chunk); + } + + splitBatchSaveData.forEach((data, index) => { + // @ts-ignore + expect(batchSaveSpy?.mock.calls[index][0]).toEqual(data); + }); + + } else { + expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, model); + } + } + }); + + it('should get, map correctly the data and batchDelete the result', async () => { + await fetchData(nangoMock); + + for (const model of models) { + const batchDeleteData = await nangoMock.getBatchDeleteData(model); + if (batchDeleteData && batchDeleteData.length > 0) { + expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, model); + } + } + }); +}); diff --git a/integrations/intercom/types.ts b/integrations/intercom/types.ts index 1ad8c754a..f74496e93 100644 --- a/integrations/intercom/types.ts +++ b/integrations/intercom/types.ts @@ -416,3 +416,10 @@ export interface IntercomConversationsResponse { total_count: number; conversations: IntercomConversation[]; } + +export interface IntercomDeleteContactResponse { + id: string; + external_id?: string; + type: 'contact'; + deleted: boolean; +}