From f25e8ca228efd5adf3de5b013dc7dbd57ac39447 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 17 Dec 2024 12:29:06 +0200 Subject: [PATCH 1/4] fix(license): add license (#158) ## Describe your changes ## Issue ticket number and link ## Checklist before requesting a review (skip if just adding/editing APIs & templates) - [ ] I added tests, otherwise the reason is: - [ ] External API requests have `retries` - [ ] Pagination is used where appropriate - [ ] 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) - [ ] If a sync requires metadata the `nango.yaml` has `auto_start: false` - [ ] If the sync is a `full` sync then `track_deletes: true` is set - [ ] I followed the best practices and guidelines from the [Writing Integration Scripts](/NangoHQ/integration-templates/blob/main/WRITING_INTEGRATION_SCRIPTS.md) doc --- LICENSE | 44 ++++++++++++++++++++++++++++++++++++++++++++ LICENSE_SHORT | 1 + 2 files changed, 45 insertions(+) create mode 100644 LICENSE create mode 100644 LICENSE_SHORT diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..682d649c --- /dev/null +++ b/LICENSE @@ -0,0 +1,44 @@ +Elastic License 2.0 (ELv2) + +**Acceptance** +By using the software, you agree to all of the terms and conditions below. + +**Copyright License** +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below + +**Limitations** +You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. + +**Patents** +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +**Notices** +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. + +**No Other Rights** +These terms do not imply any licenses other than those expressly granted in these terms. + +**Termination** +If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. + +**No Liability** +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. + +**Definitions** +The _licensor_ is the entity offering these terms, and the _software_ is the software the licensor makes available under these terms, including any portion of it. + +_you_ refers to the individual or entity agreeing to these terms. + +_your company_ is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. _control_ means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +_your licenses_ are all the licenses granted to you for the software under these terms. + +_use_ means anything you do with the software requiring one of your licenses. + +_trademark_ means trademarks, service marks, and similar rights. diff --git a/LICENSE_SHORT b/LICENSE_SHORT new file mode 100644 index 00000000..69bc660c --- /dev/null +++ b/LICENSE_SHORT @@ -0,0 +1 @@ +Copyright (c) 2024 Nango Inc, all rights reserved. From dbcc52517aa4f10433c9c832e66522dd26c8c175 Mon Sep 17 00:00:00 2001 From: nalanj <5594+nalanj@users.noreply.github.com> Date: Wed, 18 Dec 2024 03:26:46 -0500 Subject: [PATCH 2/4] fix(test): Set limit on first page (#160) ## Describe your changes The first page wasn't including a limit param, and that caused issues for tests. ## Issue ticket number and link ## Checklist before requesting a review (skip if just adding/editing APIs & templates) - [ ] I added tests, otherwise the reason is: - [ ] External API requests have `retries` - [ ] Pagination is used where appropriate - [ ] 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) - [ ] If a sync requires metadata the `nango.yaml` has `auto_start: false` - [ ] If the sync is a `full` sync then `track_deletes: true` is set - [ ] I followed the best practices and guidelines from the [Writing Integration Scripts](/NangoHQ/integration-templates/blob/main/WRITING_INTEGRATION_SCRIPTS.md) doc --- vitest.setup.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vitest.setup.ts b/vitest.setup.ts index f96a69ea..f945ea95 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -128,6 +128,12 @@ class NangoActionMock { const paginateInBody = ['post', 'put', 'patch'].includes(args.method.toLowerCase()); const updatedBodyOrParams = paginateInBody ? (args.data as Record) || {} : args.params || {}; + if (args.paginate['limit']) { + const limitParameterName = args.paginate.limit_name_in_request!; + + updatedBodyOrParams[limitParameterName] = args.paginate['limit']; + } + if (args.paginate?.type === 'cursor') { yield* this.cursorPaginate(args, updatedBodyOrParams, paginateInBody); } else if (args.paginate?.type === 'link') { From cfeeae65e2a95eda24c3bf6a8d444a07debe57f9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 18 Dec 2024 11:00:47 +0200 Subject: [PATCH 3/4] fix(pagination): rename and update action param (#161) ## Describe your changes As pointed out in https://github.com/NangoHQ/integration-templates/pull/160/files#r1888871550 ## Issue ticket number and link ## Checklist before requesting a review (skip if just adding/editing APIs & templates) - [ ] I added tests, otherwise the reason is: - [ ] External API requests have `retries` - [ ] Pagination is used where appropriate - [ ] 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) - [ ] If a sync requires metadata the `nango.yaml` has `auto_start: false` - [ ] If the sync is a `full` sync then `track_deletes: true` is set - [ ] I followed the best practices and guidelines from the [Writing Integration Scripts](/NangoHQ/integration-templates/blob/main/WRITING_INTEGRATION_SCRIPTS.md) doc --- flows.yaml | 5 +++-- integrations/front/actions/conversation.ts | 1 + .../Conversation/batchDelete.json | 0 .../Conversation/batchSave.json | 0 .../{list-conversations.json => conversations.json} | 0 integrations/front/nango.yaml | 5 +++-- .../syncs/{list-conversations.md => conversations.md} | 10 +++++----- .../syncs/{list-conversations.ts => conversations.ts} | 0 ...nversations.test.ts => front-conversations.test.ts} | 6 +++--- 9 files changed, 15 insertions(+), 12 deletions(-) rename integrations/front/mocks/{list-conversations => conversations}/Conversation/batchDelete.json (100%) rename integrations/front/mocks/{list-conversations => conversations}/Conversation/batchSave.json (100%) rename integrations/front/mocks/nango/get/proxy/conversations/{list-conversations.json => conversations.json} (100%) rename integrations/front/syncs/{list-conversations.md => conversations.md} (91%) rename integrations/front/syncs/{list-conversations.ts => conversations.ts} (100%) rename integrations/front/tests/{front-list-conversations.test.ts => front-conversations.test.ts} (90%) diff --git a/flows.yaml b/flows.yaml index eba21470..4078bf89 100644 --- a/flows.yaml +++ b/flows.yaml @@ -3497,16 +3497,17 @@ integrations: is_escalated: boolean front: syncs: - list-conversations: + conversations: runs: every day description: List the conversations in the company in reverse chronological order. output: Conversation endpoint: method: GET path: /conversations + group: Conversations track_deletes: true sync_type: full - version: 1.0.1 + version: 1.0.2 actions: conversation: description: >- diff --git a/integrations/front/actions/conversation.ts b/integrations/front/actions/conversation.ts index 17bfe881..3ba7f44c 100644 --- a/integrations/front/actions/conversation.ts +++ b/integrations/front/actions/conversation.ts @@ -11,6 +11,7 @@ export default async function runAction(nango: NangoAction, input: SingleConvers paginate: { type: 'link', response_path: '_results', + limit_name_in_request: 'limit', link_path_in_response_body: 'next', limit: 100 } diff --git a/integrations/front/mocks/list-conversations/Conversation/batchDelete.json b/integrations/front/mocks/conversations/Conversation/batchDelete.json similarity index 100% rename from integrations/front/mocks/list-conversations/Conversation/batchDelete.json rename to integrations/front/mocks/conversations/Conversation/batchDelete.json diff --git a/integrations/front/mocks/list-conversations/Conversation/batchSave.json b/integrations/front/mocks/conversations/Conversation/batchSave.json similarity index 100% rename from integrations/front/mocks/list-conversations/Conversation/batchSave.json rename to integrations/front/mocks/conversations/Conversation/batchSave.json diff --git a/integrations/front/mocks/nango/get/proxy/conversations/list-conversations.json b/integrations/front/mocks/nango/get/proxy/conversations/conversations.json similarity index 100% rename from integrations/front/mocks/nango/get/proxy/conversations/list-conversations.json rename to integrations/front/mocks/nango/get/proxy/conversations/conversations.json diff --git a/integrations/front/nango.yaml b/integrations/front/nango.yaml index 635ed911..a22fe55b 100644 --- a/integrations/front/nango.yaml +++ b/integrations/front/nango.yaml @@ -1,16 +1,17 @@ integrations: front: syncs: - list-conversations: + conversations: runs: every day description: List the conversations in the company in reverse chronological order. output: Conversation endpoint: method: GET path: /conversations + group: Conversations track_deletes: true sync_type: full - version: 1.0.1 + version: 1.0.2 actions: conversation: description: List the messages in a conversation in reverse chronological order (newest first). diff --git a/integrations/front/syncs/list-conversations.md b/integrations/front/syncs/conversations.md similarity index 91% rename from integrations/front/syncs/list-conversations.md rename to integrations/front/syncs/conversations.md index e91766cc..a08a9133 100644 --- a/integrations/front/syncs/list-conversations.md +++ b/integrations/front/syncs/conversations.md @@ -1,14 +1,14 @@ -# List Conversations +# Conversations ## General Information - **Description:** List the conversations in the company in reverse chronological order. -- **Version:** 1.0.1 +- **Version:** 1.0.2 - **Group:** Others - **Scopes:** _None_ - **Endpoint Type:** Sync -- **Code:** [github.com](https://github.com/NangoHQ/integration-templates/tree/main/integrations/front/syncs/list-conversations.ts) +- **Code:** [github.com](https://github.com/NangoHQ/integration-templates/tree/main/integrations/front/syncs/conversations.ts) ## Endpoint Reference @@ -74,8 +74,8 @@ _No request body_ ## Changelog -- [Script History](https://github.com/NangoHQ/integration-templates/commits/main/integrations/front/syncs/list-conversations.ts) -- [Documentation History](https://github.com/NangoHQ/integration-templates/commits/main/integrations/front/syncs/list-conversations.md) +- [Script History](https://github.com/NangoHQ/integration-templates/commits/main/integrations/front/syncs/conversations.ts) +- [Documentation History](https://github.com/NangoHQ/integration-templates/commits/main/integrations/front/syncs/conversations.md) diff --git a/integrations/front/syncs/list-conversations.ts b/integrations/front/syncs/conversations.ts similarity index 100% rename from integrations/front/syncs/list-conversations.ts rename to integrations/front/syncs/conversations.ts diff --git a/integrations/front/tests/front-list-conversations.test.ts b/integrations/front/tests/front-conversations.test.ts similarity index 90% rename from integrations/front/tests/front-list-conversations.test.ts rename to integrations/front/tests/front-conversations.test.ts index d0895b4b..090ba529 100644 --- a/integrations/front/tests/front-list-conversations.test.ts +++ b/integrations/front/tests/front-conversations.test.ts @@ -1,11 +1,11 @@ import { vi, expect, it, describe } from 'vitest'; -import fetchData from '../syncs/list-conversations.js'; +import fetchData from '../syncs/conversations.js'; -describe('front list-conversations tests', () => { +describe('front conversations tests', () => { const nangoMock = new global.vitest.NangoSyncMock({ dirname: __dirname, - name: 'list-conversations', + name: 'conversations', Model: 'Conversation' }); From 70080800c8ad09181a643f4f20f85c4e5634dbbe Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 18 Dec 2024 11:24:46 +0200 Subject: [PATCH 4/4] feat(gorgias): [ext-251] gorgias ticket (#162) ## Describe your changes Clean up PR to add gorgias ticket #157 ## Issue ticket number and link ## Checklist before requesting a review (skip if just adding/editing APIs & templates) - [ ] I added tests, otherwise the reason is: - [ ] External API requests have `retries` - [ ] Pagination is used where appropriate - [ ] 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) - [ ] If a sync requires metadata the `nango.yaml` has `auto_start: false` - [ ] If the sync is a `full` sync then `track_deletes: true` is set - [ ] I followed the best practices and guidelines from the [Writing Integration Scripts](/NangoHQ/integration-templates/blob/main/WRITING_INTEGRATION_SCRIPTS.md) doc --- flows.yaml | 48 ++++- integrations/avalara-sandbox/schema.zod.ts | 1 - integrations/bill-sandbox/schema.zod.ts | 1 - integrations/docusign-sandbox/schema.zod.ts | 1 - integrations/github-app/schema.zod.ts | 114 +++++++++++ integrations/gorgias/actions/create-ticket.md | 142 +++++++++++++ integrations/gorgias/actions/create-ticket.ts | 193 ++++++++++++++++++ .../gorgias/fixtures/create-ticket.json | 35 ++++ integrations/gorgias/nango.yaml | 48 ++++- integrations/gorgias/schema.zod.ts | 43 +++- integrations/gorgias/syncs/tickets.md | 34 +-- integrations/gorgias/types.ts | 149 +++++++++++--- .../ramp/tests/ramp-disable-user.test.ts | 22 +- integrations/ramp/tests/ramp-users.test.ts | 60 +++--- .../generate-integration-template-zod.bash | 2 +- scripts/generate-readmes.ts | 2 +- 16 files changed, 776 insertions(+), 119 deletions(-) delete mode 120000 integrations/avalara-sandbox/schema.zod.ts delete mode 120000 integrations/bill-sandbox/schema.zod.ts delete mode 120000 integrations/docusign-sandbox/schema.zod.ts create mode 100644 integrations/github-app/schema.zod.ts create mode 100644 integrations/gorgias/actions/create-ticket.md create mode 100644 integrations/gorgias/actions/create-ticket.ts create mode 100644 integrations/gorgias/fixtures/create-ticket.json diff --git a/flows.yaml b/flows.yaml index 4078bf89..e66a6ffc 100644 --- a/flows.yaml +++ b/flows.yaml @@ -4302,6 +4302,23 @@ integrations: endpoint: method: GET path: /tickets + group: Tickets + version: 1.0.1 + actions: + create-ticket: + description: | + Creates a new ticket + input: CreateTicketInput + scopes: + - tickets:write + - account:read + - customers:write + - customers:read + endpoint: + method: POST + path: /ticket + group: Tickets + output: Ticket models: Ticket: id: number @@ -4314,15 +4331,15 @@ integrations: sms | twitter | twitter-direct-message | whatsapp | yotpo-review closed_datetime: string | null created_datetime: string | null - excerpt: string + excerpt?: string | undefined external_id: string | null from_agent: boolean - integrations: array | null + integrations?: array | null | undefined is_unread: boolean language: string | null last_message_datetime: string | null last_received_message_datetime: string | null - messages_count: number + messages_count?: number | undefined messages: Message[] meta: object | null opened_datetime: string | null @@ -4335,7 +4352,7 @@ integrations: uri: string | null decoration: object | null created_datetime: string | null - deleted_datetime: string | null + deleted_datetime?: string | null | undefined spam: boolean | null trashed_datetime: string | null updated_datetime: string | null @@ -4379,8 +4396,8 @@ integrations: public: boolean from_agent: boolean sender: RecieverSender - receiver: RecieverSender - attachments: Attachment[] + receiver: RecieverSender | null + attachments: Attachment[] | null meta: object | null headers: object | null actions: array | null @@ -4390,8 +4407,8 @@ integrations: failed_datetime: string | null last_sending_error: object | null deleted_datetime: string | null - replied_by: string | null - replied_to: string | null + replied_by?: string | null | undefined + replied_to?: string | null | undefined AssigneeUser: id: number firstname: string @@ -4419,6 +4436,21 @@ integrations: content_type: string public: boolean extra: string | null + CreateTicketInput: + customer: + phone_number: string + email?: string | undefined + ticket: + messages: CreateTicketMessage[] + CreateTicketMessage: + attachments: + - url: string + name: string + size: number + content_type: string + body_html: string + body_text: string + id: string greenhouse-basic: syncs: applications: diff --git a/integrations/avalara-sandbox/schema.zod.ts b/integrations/avalara-sandbox/schema.zod.ts deleted file mode 120000 index 26af5ab6..00000000 --- a/integrations/avalara-sandbox/schema.zod.ts +++ /dev/null @@ -1 +0,0 @@ -../avalara/schema.zod.ts \ No newline at end of file diff --git a/integrations/bill-sandbox/schema.zod.ts b/integrations/bill-sandbox/schema.zod.ts deleted file mode 120000 index 77dfbd1a..00000000 --- a/integrations/bill-sandbox/schema.zod.ts +++ /dev/null @@ -1 +0,0 @@ -../bill/schema.zod.ts \ No newline at end of file diff --git a/integrations/docusign-sandbox/schema.zod.ts b/integrations/docusign-sandbox/schema.zod.ts deleted file mode 120000 index f458ca18..00000000 --- a/integrations/docusign-sandbox/schema.zod.ts +++ /dev/null @@ -1 +0,0 @@ -../docusign/schema.zod.ts \ No newline at end of file diff --git a/integrations/github-app/schema.zod.ts b/integrations/github-app/schema.zod.ts new file mode 100644 index 00000000..8e7a88b3 --- /dev/null +++ b/integrations/github-app/schema.zod.ts @@ -0,0 +1,114 @@ +// Generated by ts-to-zod +import { z } from 'zod'; + +export const repositorySchema = z.object({ + allow_forking: z.boolean(), + archive_url: z.string(), + archived: z.boolean(), + assignees_url: z.string(), + blobs_url: z.string(), + branches_url: z.string(), + clone_url: z.string(), + collaborators_url: z.string(), + comments_url: z.string(), + commits_url: z.string(), + compare_url: z.string(), + contents_url: z.string(), + contributors_url: z.string(), + created_at: z.string(), + default_branch: z.string(), + deployments_url: z.string(), + description: z.string(), + disabled: z.boolean(), + downloads_url: z.string(), + events_url: z.string(), + fork: z.boolean(), + forks: z.number(), + forks_count: z.number(), + forks_url: z.string(), + full_name: z.string(), + git_commits_url: z.string(), + git_refs_url: z.string(), + git_tags_url: z.string(), + git_url: z.string(), + has_discussions: z.boolean(), + has_downloads: z.boolean(), + has_issues: z.boolean(), + has_pages: z.boolean(), + has_projects: z.boolean(), + has_wiki: z.boolean(), + homepage: z.string().nullable(), + hooks_url: z.string(), + html_url: z.string(), + id: z.number(), + is_template: z.boolean(), + issue_comment_url: z.string(), + issue_events_url: z.string(), + issues_url: z.string(), + keys_url: z.string(), + labels_url: z.string(), + language: z.string(), + languages_url: z.string(), + license: z.string().nullable(), + merges_url: z.string(), + milestones_url: z.string(), + mirror_url: z.string().nullable(), + name: z.string(), + node_id: z.string(), + notifications_url: z.string(), + open_issues: z.number(), + open_issues_count: z.number(), + owner: z.object({ + avatar_url: z.string(), + events_url: z.string(), + followers_url: z.string(), + following_url: z.string(), + gists_url: z.string(), + gravatar_id: z.string(), + html_url: z.string(), + id: z.number(), + login: z.string(), + node_id: z.string(), + organizations_url: z.string(), + received_events_url: z.string(), + repos_url: z.string(), + site_admin: z.boolean(), + starred_url: z.string(), + subscriptions_url: z.string(), + type: z.string(), + url: z.string() + }), + permissions: z.object({ + admin: z.boolean(), + maintain: z.boolean(), + pull: z.boolean(), + push: z.boolean(), + triage: z.boolean() + }), + private: z.boolean(), + pulls_url: z.string(), + pushed_at: z.string(), + releases_url: z.string(), + size: z.number(), + ssh_url: z.string(), + stargazers_count: z.number(), + stargazers_url: z.string(), + statuses_url: z.string(), + subscribers_url: z.string(), + subscription_url: z.string(), + svn_url: z.string(), + tags_url: z.string(), + teams_url: z.string(), + topics: z.array(z.string()), + trees_url: z.string(), + updated_at: z.string(), + url: z.string(), + visibility: z.string(), + watchers: z.number(), + watchers_count: z.number(), + web_commit_signoff_required: z.boolean() +}); + +export const repoResponseSchema = z.object({ + repositories: z.array(repositorySchema) +}); diff --git a/integrations/gorgias/actions/create-ticket.md b/integrations/gorgias/actions/create-ticket.md new file mode 100644 index 00000000..c7e64607 --- /dev/null +++ b/integrations/gorgias/actions/create-ticket.md @@ -0,0 +1,142 @@ + +# Create Ticket + +## General Information + +- **Description:** Creates a new ticket + +- **Version:** 0.0.1 +- **Group:** Others +- **Scopes:** `tickets:write, account:read, customers:write, customers:read` +- **Endpoint Type:** Action +- **Code:** [github.com](https://github.com/NangoHQ/integration-templates/tree/main/integrations/gorgias/actions/create-ticket.ts) + + +## Endpoint Reference + +### Request Endpoint + +`POST /ticket` + +### Request Query Parameters + +_No request parameters_ + +### Request Body + +```json +{ + "customer": { + "phone_number": "", + "email?": "" + }, + "ticket": { + "messages": [ + { + "attachments": { + "0": { + "url": "", + "name": "", + "size": "", + "content_type": "" + } + }, + "body_html": "", + "body_text": "", + "id": "" + } + ] + } +} +``` + +### Request Response + +```json +{ + "id": "", + "assignee_user": "", + "channel": "", + "closed_datetime": "", + "created_datetime": "", + "excerpt?": "", + "external_id": "", + "from_agent": "", + "integrations?": "", + "is_unread": "", + "language": "", + "last_message_datetime": "", + "last_received_message_datetime": "", + "messages_count?": "", + "messages": [ + { + "id": "", + "uri": "", + "message_id": "", + "integration_id": "", + "rule_id": "", + "external_id": "", + "ticket_id": "", + "channel": "", + "via": "", + "subject": "", + "body_text": "", + "body_html": "", + "stripped_text": "", + "stripped_html": "", + "stripped_signature": "", + "public": "", + "from_agent": "", + "sender": { + "id": "", + "firstname": "", + "lastname": "", + "meta": "", + "email": "", + "name": "" + }, + "receiver": "", + "attachments": "", + "meta": "", + "headers": "", + "actions": "", + "macros": "", + "created_datetime": "", + "opened_datetime": "", + "failed_datetime": "", + "last_sending_error": "", + "deleted_datetime": "", + "replied_by?": "", + "replied_to?": "" + } + ], + "meta": "", + "opened_datetime": "", + "snooze_datetime": "", + "status": "", + "subject": "", + "tags": { + "0": { + "id": "", + "name": "", + "uri": "", + "decoration": "", + "created_datetime": "", + "deleted_datetime?": "" + } + }, + "spam": "", + "trashed_datetime": "", + "updated_datetime": "", + "via": "", + "uri": "" +} +``` + +## Changelog + +- [Script History](https://github.com/NangoHQ/integration-templates/commits/main/integrations/gorgias/actions/create-ticket.ts) +- [Documentation History](https://github.com/NangoHQ/integration-templates/commits/main/integrations/gorgias/actions/create-ticket.md) + + + diff --git a/integrations/gorgias/actions/create-ticket.ts b/integrations/gorgias/actions/create-ticket.ts new file mode 100644 index 00000000..46f9d7c7 --- /dev/null +++ b/integrations/gorgias/actions/create-ticket.ts @@ -0,0 +1,193 @@ +import type { NangoAction, ProxyConfiguration, Ticket, CreateTicketInput } from '../../models'; +import { createTicketInputSchema } from '../schema.zod.js'; +import type { GorgiasCustomerResponse, GorgiasSettingsResponse, TicketAssignmentData, GorgiasCustomersResponse, GorgiasTicketResponse } from '../types'; +import { toTicket } from '../mapper/to-ticket.js'; + +export default async function runAction(nango: NangoAction, input: CreateTicketInput): Promise { + const parsedInput = createTicketInputSchema.safeParse(input); + if (!parsedInput.success) { + for (const error of parsedInput.error.errors) { + await nango.log(`Invalid input: ${error.message}`, { level: 'error' }); + } + throw new nango.ActionError({ + message: 'Invalid input provided' + }); + } + + const customer = await findOrCreateCustomer(nango, input.customer.phone_number, input.customer.email); + + const channel = await checkSmsChannel(nango); + + const ticketData = { + channel, + created_datetime: new Date().toISOString(), + customer: customer, + from_agent: false, + opened_datetime: new Date().toISOString(), + messages: input.ticket.messages.map((message) => ({ + attachments: message.attachments || [], + body_html: message.body_html, + body_text: message.body_text, + channel: 'phone', + created_datetime: new Date().toISOString(), + external_id: message.id, + from_agent: false, + sender: customer, + via: 'api' + })) + }; + + const config: ProxyConfiguration = { + // https://developers.gorgias.com/reference/create-ticket + endpoint: '/api/tickets', + retries: 10, + data: ticketData + }; + + const response = await nango.post(config); + + return toTicket(response.data, response.data.messages); +} + +/** + * Finds an existing customer in Gorgias by email or phone, or creates a new one if no match is found. + * + * @param nango - An instance of NangoAction to handle API requests. + * @param phone - The phone number to search for or associate with the customer. + * @param email - The email address to search for or associate with the customer (optional). + * @returns A Promise resolving to an object containing the customer's ID and email. + */ +async function findOrCreateCustomer(nango: NangoAction, phone: string, email?: string): Promise<{ id: number; email: string }> { + let customer: GorgiasCustomerResponse | null = null; + + // First, check if email is provided and try to find the customer by email + if (email) { + const customerConfig: ProxyConfiguration = { + // https://developers.gorgias.com/reference/list-customers + endpoint: '/api/customers', + retries: 10, + params: { email } + }; + + const response = await nango.get(customerConfig); + if (response.data.length === 0) { + await nango.log(`No customer found with email: ${email}.`, { level: 'info' }); + } else { + customer = response.data[0] || null; + } + } + + // If no customer was found by email, check by phone number + if (!customer) { + const config: ProxyConfiguration = { + // https://developers.gorgias.com/reference/list-customers + endpoint: '/api/customers', + retries: 10, + paginate: { + type: 'cursor', + cursor_path_in_response: 'meta.next_cursor', + cursor_name_in_request: 'cursor', + response_path: 'data', + limit: 100, + limit_name_in_request: 'limit' + } + }; + + for await (const paginatedCustomers of nango.paginate(config)) { + for (const customerData of paginatedCustomers) { + const customerDetailConfig: ProxyConfiguration = { + // https://developers.gorgias.com/reference/get-customer + endpoint: `/api/customers/${customerData.id}`, + retries: 10 + }; + + const customerDetailResponse = await nango.get(customerDetailConfig); + const detailedCustomer = customerDetailResponse.data; + + const emailChannel = detailedCustomer.channels.find((channel) => channel.type === 'email' && channel.address === email); + const phoneChannel = detailedCustomer.channels.find((channel) => channel.type === 'phone' && channel.address === phone); + + if (emailChannel || phoneChannel) { + customer = detailedCustomer; + break; + } + } + + if (customer) break; + } + } + + // If no customer found by email or phone, create a new one + if (!customer) { + await nango.log(`No customer found with phone: ${phone} or email: ${email}. Creating a new one.`, { level: 'info' }); + + const newCustomer = await createNewCustomer(nango, phone, email); + return { id: newCustomer.id, email: newCustomer.email }; + } + + return { id: customer.id, email: customer.email }; +} +/** + * Creates a new customer in Gorgias with the provided phone and/or email details. + * + * @param nango - An instance of NangoAction to perform the API request. + * @param phone - The phone number of the customer (optional). + * @param email - The email address of the customer (optional). + * @returns A Promise resolving to the created customer object. + */ +async function createNewCustomer(nango: NangoAction, phone?: string, email?: string): Promise { + const newCustomerData = { + channels: [...(email ? [{ type: 'email', address: email }] : []), ...(phone ? [{ type: 'phone', address: phone }] : [])] + }; + + const createConfig: ProxyConfiguration = { + // https://developers.gorgias.com/reference/create-customer// + endpoint: '/api/customers', + retries: 10, + data: newCustomerData + }; + + const createResponse = await nango.post(createConfig); + return createResponse.data; +} + +/** + * Checks if the SMS channel is enabled in Gorgias account settings. + * If the SMS channel is found, it returns "phone"; otherwise, it defaults to "email". + * + * @param nango - An instance of NangoAction to perform API requests. + * @returns A Promise resolving to "phone" if SMS is enabled, otherwise "email". + */ +async function checkSmsChannel(nango: NangoAction): Promise<'phone' | 'email'> { + const config: ProxyConfiguration = { + // https://developers.gorgias.com/reference/get-account + endpoint: '/api/account/settings', + retries: 10 + }; + + const response = await nango.get(config); + const settingsItems = response.data.data; + + for (const item of settingsItems) { + if (item.type === 'ticket-assignment') { + if (item.data && 'assignment_channels' in item.data) { + if (isTicketAssignmentData(item.data)) { + if (item.data.assignment_channels.includes('sms')) { + return 'phone'; + } + } + } + } + } + + return 'email'; +} + +function isTicketAssignmentData(data: TicketAssignmentData): data is TicketAssignmentData { + return ( + data && + typeof data === 'object' && + Array.isArray(data.assignment_channels) && + data.assignment_channels.every((channel: any) => typeof channel === 'string') + ); +} diff --git a/integrations/gorgias/fixtures/create-ticket.json b/integrations/gorgias/fixtures/create-ticket.json new file mode 100644 index 00000000..bc035ade --- /dev/null +++ b/integrations/gorgias/fixtures/create-ticket.json @@ -0,0 +1,35 @@ +{ + "customer": { + "phone_number": "0719908543", + "email": "nango@example.com" + }, + "ticket": { + "messages": [ + { + "attachments": [ + { + "url": "https://example.com/file1.png", + "name": "file1.png", + "size": 1024, + "content_type": "image/png" + }, + { + "url": "https://example.com/file2.pdf", + "name": "file2.pdf", + "size": 2048, + "content_type": "application/pdf" + } + ], + "body_html": "
Hello, this is the HTML version of the message.
", + "body_text": "Hello, this is the text version of the message.", + "id": "message123" + }, + { + "attachments": [], + "body_html": "
Another message in HTML.
", + "body_text": "Another message in text.", + "id": "message124" + } + ] + } +} diff --git a/integrations/gorgias/nango.yaml b/integrations/gorgias/nango.yaml index 5505546d..0a7bbaca 100644 --- a/integrations/gorgias/nango.yaml +++ b/integrations/gorgias/nango.yaml @@ -13,6 +13,23 @@ integrations: endpoint: method: GET path: /tickets + group: Tickets + version: 1.0.1 + actions: + create-ticket: + description: | + Creates a new ticket + input: CreateTicketInput + scopes: + - tickets:write + - account:read + - customers:write + - customers:read + endpoint: + method: POST + path: /ticket + group: Tickets + output: Ticket models: Ticket: id: number @@ -20,15 +37,15 @@ models: channel: aircall | api | chat | contact_form | email | facebook | facebook-mention | facebook-messenger | facebook-recommendations | help-center | instagram-ad-comment | instagram-comment | instagram-direct-message | instagram-mention | internal-note | phone | sms | twitter | twitter-direct-message | whatsapp | yotpo-review closed_datetime: string | null created_datetime: string | null - excerpt: string + excerpt?: string | undefined external_id: string | null from_agent: boolean - integrations: array | null + integrations?: array | null | undefined is_unread: boolean language: string | null last_message_datetime: string | null last_received_message_datetime: string | null - messages_count: number + messages_count?: number | undefined messages: Message[] meta: object | null opened_datetime: string | null @@ -41,7 +58,7 @@ models: uri: string | null decoration: object | null created_datetime: string | null - deleted_datetime: string | null + deleted_datetime?: string | null | undefined spam: boolean | null trashed_datetime: string | null updated_datetime: string | null @@ -66,8 +83,8 @@ models: public: boolean from_agent: boolean sender: RecieverSender - receiver: RecieverSender - attachments: Attachment[] + receiver: RecieverSender | null + attachments: Attachment[] | null meta: object | null headers: object | null actions: array | null @@ -77,8 +94,8 @@ models: failed_datetime: string | null last_sending_error: object | null deleted_datetime: string | null - replied_by: string | null - replied_to: string | null + replied_by?: string | null | undefined + replied_to?: string | null | undefined AssigneeUser: __extends: User email: string @@ -100,3 +117,18 @@ models: content_type: string public: boolean extra: string | null + CreateTicketInput: + customer: + phone_number: string + email?: string | undefined + ticket: + messages: CreateTicketMessage[] + CreateTicketMessage: + attachments: + - url: string + name: string + size: number + content_type: string + body_html: string + body_text: string + id: string diff --git a/integrations/gorgias/schema.zod.ts b/integrations/gorgias/schema.zod.ts index 3efb7490..cbe1d168 100644 --- a/integrations/gorgias/schema.zod.ts +++ b/integrations/gorgias/schema.zod.ts @@ -110,8 +110,8 @@ export const messageSchema = z.object({ public: z.boolean(), from_agent: z.boolean(), sender: recieverSenderSchema, - receiver: recieverSenderSchema, - attachments: z.array(attachmentSchema), + receiver: recieverSenderSchema.nullable(), + attachments: z.array(attachmentSchema).nullable(), meta: z.record(z.any()).nullable(), headers: z.record(z.any()).nullable(), actions: z.array(z.any()).nullable(), @@ -121,8 +121,8 @@ export const messageSchema = z.object({ failed_datetime: z.string().nullable(), last_sending_error: z.record(z.any()).nullable(), deleted_datetime: z.string().nullable(), - replied_by: z.string().nullable(), - replied_to: z.string().nullable() + replied_by: z.union([z.string(), z.undefined()]).optional().nullable(), + replied_to: z.union([z.string(), z.undefined()]).optional().nullable() }); export const ticketSchema = z.object({ @@ -153,15 +153,18 @@ export const ticketSchema = z.object({ ]), closed_datetime: z.string().nullable(), created_datetime: z.string().nullable(), - excerpt: z.string(), + excerpt: z.union([z.string(), z.undefined()]).optional(), external_id: z.string().nullable(), from_agent: z.boolean(), - integrations: z.array(z.any()).nullable(), + integrations: z + .union([z.array(z.any()), z.undefined()]) + .optional() + .nullable(), is_unread: z.boolean(), language: z.string().nullable(), last_message_datetime: z.string().nullable(), last_received_message_datetime: z.string().nullable(), - messages_count: z.number(), + messages_count: z.union([z.number(), z.undefined()]).optional(), messages: z.array(messageSchema), meta: z.record(z.any()).nullable(), opened_datetime: z.string().nullable(), @@ -175,7 +178,7 @@ export const ticketSchema = z.object({ uri: z.string().nullable(), decoration: z.record(z.any()).nullable(), created_datetime: z.string().nullable(), - deleted_datetime: z.string().nullable() + deleted_datetime: z.union([z.string(), z.undefined()]).optional().nullable() }) ), spam: z.boolean().nullable(), @@ -217,3 +220,27 @@ export const ticketSchema = z.object({ ]), uri: z.string() }); + +export const createTicketMessageSchema = z.object({ + attachments: z.array( + z.object({ + url: z.string(), + name: z.string(), + size: z.number(), + content_type: z.string() + }) + ), + body_html: z.string(), + body_text: z.string(), + id: z.string() +}); + +export const createTicketInputSchema = z.object({ + customer: z.object({ + phone_number: z.string(), + email: z.union([z.string(), z.undefined()]).optional() + }), + ticket: z.object({ + messages: z.array(createTicketMessageSchema) + }) +}); diff --git a/integrations/gorgias/syncs/tickets.md b/integrations/gorgias/syncs/tickets.md index 48cf19e2..f58bf04e 100644 --- a/integrations/gorgias/syncs/tickets.md +++ b/integrations/gorgias/syncs/tickets.md @@ -5,7 +5,7 @@ - **Description:** Fetches a list of tickets with their associated messages -- **Version:** 0.0.1 +- **Version:** 1.0.1 - **Group:** Others - **Scopes:** `tickets:read` - **Endpoint Type:** Sync @@ -38,15 +38,15 @@ _No request body_ "channel": "", "closed_datetime": "", "created_datetime": "", - "excerpt": "", + "excerpt?": "", "external_id": "", "from_agent": "", - "integrations": "", + "integrations?": "", "is_unread": "", "language": "", "last_message_datetime": "", "last_received_message_datetime": "", - "messages_count": "", + "messages_count?": "", "messages": [ { "id": "", @@ -74,24 +74,8 @@ _No request body_ "email": "", "name": "" }, - "receiver": { - "id": "", - "firstname": "", - "lastname": "", - "meta": "", - "email": "", - "name": "" - }, - "attachments": [ - { - "url": "", - "name": "", - "size": "", - "content_type": "", - "public": "", - "extra": "" - } - ], + "receiver": "", + "attachments": "", "meta": "", "headers": "", "actions": "", @@ -101,8 +85,8 @@ _No request body_ "failed_datetime": "", "last_sending_error": "", "deleted_datetime": "", - "replied_by": "", - "replied_to": "" + "replied_by?": "", + "replied_to?": "" } ], "meta": "", @@ -117,7 +101,7 @@ _No request body_ "uri": "", "decoration": "", "created_datetime": "", - "deleted_datetime": "" + "deleted_datetime?": "" } }, "spam": "", diff --git a/integrations/gorgias/types.ts b/integrations/gorgias/types.ts index 26cf3338..67a1382c 100644 --- a/integrations/gorgias/types.ts +++ b/integrations/gorgias/types.ts @@ -64,16 +64,16 @@ export interface GorgiasTicketResponse { spam: boolean; customer: Customer; assignee_user: AssigneeUser | null; - assignee_user_id: number; - assignee_team: any; + assignee_user_id: number | null; + assignee_team: object | null; assignee_team_id: number | null; - language: string; - subject: string; - excerpt: string; + language: string | null; + subject: string | null; + excerpt?: string; meta: any; tags: Tag[]; - integrations: []; - messages_count: number; + integrations?: []; + messages_count?: number; messages: GorgiasMessageResponse[]; created_datetime: string; opened_datetime: string | null; @@ -97,13 +97,13 @@ interface Customer { lastname: string; meta: { name_set_via: string }; channels: Channel[]; - data: any; - customer: any; - integrations: Record; + data: object | null; + customer: object | null; + integrations: object; external_id: string | null; note: string | null; - external_data: Record; - ecommerce_data: Record; + external_data?: Record; + ecommerce_data?: Record; } interface AssigneeUser { @@ -124,7 +124,7 @@ interface Tag { }; created_datetime?: string | null; deleted_datetime?: string | null; - uri?: string | null; + uri: string | null; } interface SenderReciever { @@ -203,12 +203,12 @@ export interface GorgiasMessageResponse { intents: []; rule_id: number | null; from_agent: boolean; - receiver: SenderReciever; - subject: string; - body_text: string; - body_html: string; - stripped_text: string; - stripped_html: string; + receiver: SenderReciever | null; + subject: string | null; + body_text: string | null; + body_html: string | null; + stripped_text: string | null; + stripped_html: string | null; stripped_signature: string | null; attachments: GorgiasAttachementResponse[] | null; actions: MessageAction[]; @@ -220,7 +220,7 @@ export interface GorgiasMessageResponse { opened_datetime: string | null; last_sending_error: LastSendingError | null; is_retriable: boolean; - deleted_datetime?: string | null; + deleted_datetime: string | null; replied_by: string | null; replied_to: string | null; macros: [] | null; @@ -235,10 +235,12 @@ export interface GorgiasAttachementResponse { extra: string; } interface MessageSource { - type: 'email'; - to: { name: string; address: string }[]; - from: { name: string; address: string }; - extra: { include_thread: boolean }; + type: string; + to?: { name: string | null; address: string }[]; + cc?: { name: string | null; address: string }[]; + bcc?: { name: string | null; address: string }; + from?: { name: string | null; address: string }; + extra?: { include_thread: boolean }; } interface MessageAction { @@ -259,6 +261,7 @@ interface LastSendingError { interface ReplyOptions { email: { answerable: boolean }; 'internal-note': { answerable: boolean }; + phone: { answerable: boolean }; } interface Channel { @@ -272,3 +275,101 @@ interface Channel { user: { id: number; name: string | null }; customer: { id: number; name: string | null }; } + +export interface GorgiasCustomersResponse { + id: number; + external_id: string | null; + active: boolean; + email: string; + name: string | null; + firstname: string; + lastname: string; + language: string | null; + timezone: string | null; + created_datetime: string; + updated_datetime: string; + meta: object | null; + data: object | null; + customer: object | null; + integrations: object; + note: string | null; + custom_fields: object; +} + +export interface GorgiasCustomerResponse extends GorgiasCustomersResponse { + channels: Channel[]; +} + +interface SatisfactionSurveyData { + survey_interval: number; + survey_email_html: string; + survey_email_text: string; + send_survey_for_chat: boolean; + send_survey_for_email: boolean; + send_survey_for_help_center: boolean; + send_survey_for_contact_form: boolean; +} + +interface BusinessHoursData { + timezone: string; + business_hours: { + days: string; + to_time: string; + from_time: string; + }[]; +} + +export interface TicketAssignmentData { + unassign_on_reply: boolean; + assignment_channels: string[]; + auto_assign_to_teams: boolean; + max_user_chat_ticket: number; + max_user_non_chat_ticket: number; +} + +interface ViewsOrderingData { + views: object; + views_top: object; + views_bottom: object; + view_sections: object; +} + +interface AccessData { + signup_mode: string; + allowed_domains: string[]; + google_sso_enabled: boolean; + office365_sso_enabled: boolean; +} + +interface ViewsVisibilityData { + hidden_views: string[]; +} + +interface AutoMergeData { + tickets: { + enabled: boolean; + merging_window_days: number; + }; +} + +interface DefaultIntegrationData { + email: number; +} + +interface SettingsItem { + id: number; + type: string; + data: + | SatisfactionSurveyData + | BusinessHoursData + | TicketAssignmentData + | ViewsOrderingData + | AccessData + | ViewsVisibilityData + | AutoMergeData + | DefaultIntegrationData; +} + +export interface GorgiasSettingsResponse { + data: SettingsItem[]; +} diff --git a/integrations/ramp/tests/ramp-disable-user.test.ts b/integrations/ramp/tests/ramp-disable-user.test.ts index c729239b..ae654b43 100644 --- a/integrations/ramp/tests/ramp-disable-user.test.ts +++ b/integrations/ramp/tests/ramp-disable-user.test.ts @@ -3,17 +3,17 @@ import { vi, expect, it, describe } from 'vitest'; import runAction from '../actions/disable-user.js'; describe('ramp disable-user tests', () => { - const nangoMock = new global.vitest.NangoActionMock({ - dirname: __dirname, - name: "disable-user", - Model: "SuccessResponse" - }); + const nangoMock = new global.vitest.NangoActionMock({ + dirname: __dirname, + name: 'disable-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(); + 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); - }); + expect(response).toEqual(output); + }); }); diff --git a/integrations/ramp/tests/ramp-users.test.ts b/integrations/ramp/tests/ramp-users.test.ts index b25fefa4..9e1b0a75 100644 --- a/integrations/ramp/tests/ramp-users.test.ts +++ b/integrations/ramp/tests/ramp-users.test.ts @@ -3,43 +3,43 @@ import { vi, expect, it, describe } from 'vitest'; import fetchData from '../syncs/users.js'; describe('ramp users tests', () => { - const nangoMock = new global.vitest.NangoSyncMock({ - dirname: __dirname, - name: "users", - Model: "User" - }); + const nangoMock = new global.vitest.NangoSyncMock({ + dirname: __dirname, + name: 'users', + Model: 'User' + }); - const models = 'User'.split(','); - const batchSaveSpy = vi.spyOn(nangoMock, 'batchSave'); + 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); + it('should get, map correctly the data and batchSave the result', async () => { + await fetchData(nangoMock); - for (const model of models) { - const expectedBatchSaveData = await nangoMock.getBatchSaveData(model); + for (const model of models) { + const expectedBatchSaveData = await nangoMock.getBatchSaveData(model); - const spiedData = batchSaveSpy.mock.calls.flatMap(call => { - if (call[1] === model) { - return call[0]; - } + const spiedData = batchSaveSpy.mock.calls.flatMap((call) => { + if (call[1] === model) { + return call[0]; + } - return []; - }); + return []; + }); - const spied = JSON.parse(JSON.stringify(spiedData)); + const spied = JSON.parse(JSON.stringify(spiedData)); - expect(spied).toStrictEqual(expectedBatchSaveData); - } - }); + expect(spied).toStrictEqual(expectedBatchSaveData); + } + }); - it('should get, map correctly the data and batchDelete the result', async () => { - await fetchData(nangoMock); + 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); - } - } - }); + 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/scripts/generate-integration-template-zod.bash b/scripts/generate-integration-template-zod.bash index da70d1d2..c1a2c1a8 100755 --- a/scripts/generate-integration-template-zod.bash +++ b/scripts/generate-integration-template-zod.bash @@ -8,4 +8,4 @@ else cd .. fi -bash scripts/integration-command.bash "npx nango generate && npx ts-to-zod .nango/schema.ts _CURRENT_INTEGRATION_/schema.zod.ts" "false" "${integrations[@]}" +bash scripts/integration-command.bash "npx nango generate && npx ts-to-zod .nango/schema.ts _CURRENT_INTEGRATION_/schema.zod.ts" "true" "${integrations[@]}" diff --git a/scripts/generate-readmes.ts b/scripts/generate-readmes.ts index 5da045cf..b9958acc 100644 --- a/scripts/generate-readmes.ts +++ b/scripts/generate-readmes.ts @@ -22,7 +22,7 @@ for (const integration of integrations) { for (const [type, integration, key, config] of toGenerate) { try { - const filename = `integrations/${integration}/actions/${key}.md`; + const filename = `integrations/${integration}/${type}s/${key}.md`; let markdown; try {